« poprzedni punkt 

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.


« poprzedni punkt