5. Pliki - krótkie wprowadzenie
Operacje wprowadzania danych z plików i zapisywania danych do plików są
realizowane w Javie za pomocą tzw. klas strumieniowych z pakietu java.io. Omówimy
je dokładnie w przyszłym semestrze. Teraz zajmiemy się wycinkiem tej problematyki,
skrótowo i niejako czysto "instruktażowo".
Plik - to ciąg bajtów zapisanych na dysku lub w innej fizycznie trwałej formie
Zauważmy: informacja którą przetwarzają programy - to ciągi bajtów. W trakcie
wykonania programu informacja taka jest umieszczona w pamięci operacyjnej.
Po zakończeniu programu - zajmowana pamięć udostępniana jest innym programom.
Informacja ginie. Sposobem na jej bardziej trwałe zapamiętanie jest właśnie
zapis do pliku. Zapisana informacja może być później odtworzona - przez odczytanie
jej z pliku.
Ogólnie, pliki jako ciągi bajtów (powiemy: pliki bajtowe) są w Javie reprezentowane przez obiekty
klas FileInputStream ( pliki wejściowe - z których wczytujemy dane) i FileOutputStream
( pliki wyjściowe - do których zapisujemy dane).
Przygotowanie pliku do przetwarzania przez program nazywa się otwarciem pliku.
W Javie pliki są otwierane automatycznie przy tworzeniu obiektów plikowych
(czyli obiektów oznaczających pliki, w tym obiektów wspomnianych wyżej klas FileInputStream
i FileOutputStream).
Obiekty-pliki bajtowe możemy tworzyć za pomocą konstruktorów klas
FileInputStream i
FileOutputStream, podając jako argument nazwę pliku.
Np.
FileInputStream in = new FileInputStream("Program1.java");
FileOutputStream in = new FileOutputStream("Program2.java");
Z plików bajtowych możemy czytać bajty za pomocą metody int read() i możemy do nich zapisywać bajty za pomocą metody write(int).
Zwróćmy uwagę - bajty doskonale mieszczą się w zmiennej typu byte, ale read()
zwraca wartość typu int, gdyż przy próbie czytania bajtów spoza końca pliku
musi jakoś poinformować o końcu pliku. Umownie zwraca wtedy wartość -1 (typu
int), co oczywiście jest zupełnie inną wartością niż wszelkie możliwe wartości
bajtów.
Po wykonaniu operacji na pliku powinniśmy plik zamknąć, co np. powoduje
ostateczny, fizyczny zapis informacji, być może do tego momentu będącej jeszcze
w buforach systemowych oraz inne działania porządkowe na poziomie systemu
operacyjnego (niekiedy np. związane z możliwością udostępnienia pliku innym
programom, lub z umożliwieniem otwarcia przez nasz program innych plików).
Do zamykania plików służy metoda close().
Możemy teraz napisać program, który - bajt po bajcie - kopiuje dowolny plik
wejściowy do dowolnego pliku wyjściowego. Nazwy plików podajemy jako argumenty
wywołania programu.
import java.io.*;
public class CopyFile {
public static void main(String[] args) {
FileInputStream in; // plik wejściowy
FileOutputStream out; // plik wyjściowy
try {
in = new FileInputStream(args[0]);
out = new FileOutputStream(args[1]);
int c;
while ((c = in.read()) != -1) out.write(c); // kopiowanie
in.close();
out.close();
} catch(ArrayIndexOutOfBoundsException exc) { // brak argumentu
System.out.println("Syntax: CopyFile in out");
System.exit(1);
} catch(FileNotFoundException exc) { // nieznany plik
System.out.println("Plik " + args[0] + " nie istnieje.");
System.exit(2);
} catch(IOException exc) { // inny błąd wejścia- wyjścia
System.out.println(exc.toString());
System.exit(3);
}
}
}
Każdy plik jest sekwencją bajtów. Ale znaczenie bajtów może być bardzo różne.
Mogą to być np. binarne reprezentacje jakichś liczb, albo mogą to być znaki
(wtedy będziemy mówić o plikach tekstowych).
Zwróćmy jednak uwagę, że w Javie
znaki są przedstawiane w Unicodzie (czyli
jako wartości dwubajtowe). Jeżeli tekst w pliku zapisany jest w ten właśnie sposób
- to nie ma problemu. Ale często pliki tekstowe zapisywane są w różnych systemach
kodowania, niekoniecznie w Unicodzie. Sposób kodowania znaków tekstu nazywa
się stroną kodową. Np. wiele polskich dokumentów HTML zapisanych jest z wykorzystaniem
strony kodowej ISO8859-2, inne - z wykorzystaniem strony Cp1250 (inaczej
zwanej Windows 1250). To oczywiście nie jest Unicode - znaki zajmują 1 bajt.
W każdym systemie operacyjnym możemy też ustawić tzw. domyślną stronę kodową,
która będzie wykorzystywana np. przy wczytywaniu i zapisie plików przez systemowe
edytory tekstu. Np. w systemie Windows taką domyślną stroną kodową najczęsciej jest - w polskich warunkach - Cp1250.
Przy wczytywaniu Java musi dokonać przekodowania plików zapisanych w domyślnej
stronie kodowej na Unicode, a przy zapisie wykonać operację odwrotną - przekodowania
z Unicodu do domyślnej strony kodowej. Metody klas FileInputStream i FileOutputStream
- nie wykonują tego zadania (czytają i piszą bajt po bajcie, co w przypadku
plików tekstowych może powodować utratę informacji).
Zobaczmy przykład.
Poniższy program.
import java.io.*;
public class ReadBytesAsChars {
public static void main(String[] args) {
StringBuffer cont = new StringBuffer();
try {
FileInputStream in = new FileInputStream(args[0]);
int c;
while ((c = in.read()) != -1) cont.append((char) c);
in.close();
} catch(Exception exc) {
System.out.println(exc.toString());
System.exit(1);
}
String s = cont.toString();
System.out.println(s);
}
}
czyta plik tekstowy i zapisuje jego zawartość w łańcuchu znakowym (String),
po czym wypisuje na konsoli ten łancuch znakowy. Jeśli przeczytaliśmy z
pliku zapisanego w Cp1250 następujący tekst:
Początek
pogłębienia
znajomości
Javy
to na konsoli uzyskamy:
Pocz?tek
pog??bienia
znajomo?ci
Javy
Takich strat informacji nie będzie, jeśli do czytania plików wykorzystamy
obiekt klasy FileReader, a do zapisywania - FileWriter, bowiem klasy te zapewniają konwersje między domyślną stroną kodową systemu operacyjnego i Unicodem
Do przetwarzania plików tekstowych należy wykorzystywać klasy FileReader i FileWriter
Poprzedni przykład możemy teraz zapisać tak:
import java.io.*;
public class ReadByReader {
public static void main(String[] args) {
StringBuffer cont = new StringBuffer();
try {
FileReader in = new FileReader(args[0]);
int c;
while ((c = in.read()) != -1) cont.append((char) c);
in.close();
} catch(Exception exc) {
System.out.println(exc.toString());
System.exit(1);
}
String s = cont.toString();
System.out.println(s);
}
}
Przy przetwarzaniu plików zetkniemy się także z kwestią efektywności.
Np. przy czytaniu dużych plików tekstowych należy unikać bezpośredniego
czytania za pomocą klasy FileReader, bowiem każde odczytanie znaku może powodować
fizyczne odwołanie do pliku (to samo dotyczy zapisu i klasy FileWriter).
Operacje fizycznych odwołań do pliku (dysku) są czasochłonne. Aby je ograniczyć - stosujemy tzw. buforowanie. W pamięci operacyjnej
wydzielany jest duży obszar pamięci, który zapełniany jest przez jednorazowe
fizyczne odwołanie do pliku. Instrukcje czytania pliku pobierają informacje
z tego bufora. Gdy bufor jest pusty - następuje kolejne jego wypełnienie poprzez
fizyczne odwołanie do pliku. W ten sposób liczba fizycznych odwołań do pliku
(do dysku) jest mniejsza niż liczba zapisanych w programie instrukcji czytania
danych.
W Javie do buforowania wejściowych plików tekstowych stosujemy klasę BufferedReader.
Ale klasa ta nie pozwala - przy tworzeniu obiektów - bezpośrednio, w konstruktorze,
podawać źródła danych (np. nazwy pliku).
Żródło to podajemy przy tworzeniu obiektu typu FileReader, a po to,
żeby uzyskać buforowanie, "opakowujemy" FileReader - BufferedReaderem.
Wygląda to tak:
// tu powstaje związek z fizycznym źródłem
FileReader fr = new FileReader("plik.txt");
// tu dodajemy "opakowanie",
umożliwiające buforowanie
BufferedReader br = new BufferedReader(fr);
//... teraz wszelkie odwołania czytania itp. kierujemy do obiektu
br
Dodatkowo w klasie BufferedReader zdefiniowano wygodną metodę czytania wierszy pliku:
readLine()
która zwraca kolejny wiersz jako String lub null jeśli wystąpił koniec pliku
Zarys czytania:
try {
String line;
FileReader fr = new FileReader(fname);
// fname jest nazwą pliku
BufferedReader br = new BufferedReader(fr);
while ((line = br.readLine()) !=
null) { // kolejny wiersz pliku: metoda readLine ...
// tu robimy coś z wierszami pliku }
br.close(); // zamknięcie pliku
}
catch (IOException e) {
System.err.println(e);
}
Np. metodę main w programie przykładowym z wykładu 12 (klasa TocTest,
testująca odnajdywanie nagłówków umieszczonych pomiędzy znacznikami <h2>
i </h2> za pomocą klasy Toc) możemy teraz napisać w takiej formie,
która umożliwi przetworzenie w ten sposób pliku HTML podanego jako argument
wywołania programu:
import java.io.*;
class TocTest {
public static void main(String[] args) {
final String ls = System.getProperty("line.separator");
StringBuffer doc = new StringBuffer();
try {
FileReader fr = new FileReader(args[0]);
BufferedReader br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) doc.append(line).append('\n');
br.close();
} catch (Exception exc) { System.out.println(exc); System.exit(1); }
System.out.println(new Toc(doc.toString()).getToc());
}
}
Proszę samodzielnie połączyć nową klasę TocTest
z klasą Toc przedstawioną w wykładzie 12 i zobaczyć jak działa na przykładzie
różnych (prostych!) dokumentów HTML.
Wszystko co powiedziano o buforowaniu tekstowych plików
wejściwych dotyczy również buforowania tekstowych plików wyjściowych.
W tym przypadku stosujemy klasę BufferedWriter.
Proszę zapoznać się w dokumentacji Javy z klasami FileReader, FileWriter, BufferedReader i BufferedWriter.
|