PO Strumienie

Z Studia Informatyczne
Wersja z dnia 17:28, 18 sie 2006 autorstwa Jsroka (dyskusja | edycje)
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)
Przejdź do nawigacjiPrzejdź do wyszukiwania

System wejścia/wyjścia

Opracowanie niewielkiego, a zarazem spójnego systemu wejścia/wyjścia (ang. input/output) nie jest proste. Trudności biorą się z konieczności obsłużenia wielu możliwości. Dane mogą być wysyłane i pobierane z różnych mediów, jak konsola, pliki, połączenia sieciowe, łącza między procesami, itp. W każdym przypadku obsługa danych może przebiegać na wiele sposobów, np. sekwencyjnie, ze swobodnym dostępem, poprzez bufor, binarnie, znakowo czy linia po linii. Co więcej czasami dane muszą być przetwarzane w trakcie przesyłania, np. kompresowane lub szyfrowane. Dzięki zastosowaniu wzorca Dekorator (ang. Decorator) w Javie nie nastąpiła eksplozja liczby klasy obsługi wejścia/wyjścia, chociaż początkowo ich ilość może przytłaczać.

Strumienie

W większości języków programowania biblioteki wejścia/wyjścia ukrywają szczegóły obsługi poszczególnych mediów pod abstrakcją strumienia (ang. stream). Strumienie są używane zarówno do wysyłania/zapisywania jak i pobierania/odczytywania porcji danych danych. Główną zaletą takiego podejścia jest jego uniwersalność.

W Javie hierarchia strumieni oparta jest o cztery klasy InputStream, OutputStream, Reader i Writer. InputStream i Reader reprezentują strumienie danych wejściowych, a OutputStream i Writer strumienie danych wyjściowych. Para InputStream i OutputStream jest przeznaczona do obsługi danych binarnych. Reader i Writer dodano do języka w wersji 1.1 i służą do obsługi danych znakowych. Strumienie znakowe oferują podobną funkcjonalność co binarne. Jeżeli jest to możliwe należy używać klas z hierarchii Reader/Writer. W niektórych zastosowaniach (np. kompresja) posługiwanie się danymi binarnymi jest jednak bardziej naturalne, dlatego strumienie znakowe nie zastępują strumieni binarnych, ale je uzupełniają. Możliwe jest przy tym bardzo łatwa konwersja strumieni binarnych na znakowe.

Strumienie dla poszczególnych mediów

Strumienie ujednolicają obsługę poszczególnych rodzajów mediów. Standardowe biblioteki Javy zawierają klasy reprezentujące strumienie wejściowe i wyjściowe na:

  • pliku,
  • tablicy bajtów/znaków,
  • obiekcie String oraz
  • łączu (ang. pipe) służącym do komunikacji procesów.

Dodatkowo dalsze strumienie można uzyskać bezpośrednio z obiektów reprezentujących niektóre media, np. z gniazda sieciowego czy zasobu sieci WWW wskazanego przez adres URL.

Strumienie ze standardowych bibliotek Javy do obsługi różnych rodzajów mediów
Podklasy InputStream i OutputStream Podklasy Reader i Writer Opis
FileInputStream i FileOutputStream FileReader i FileWriter Pozwalają odczytywać i zapisywać pliki dyskowe. Jako parametr konstruktora przekaż nazwę pliku dyskowego lub wskazujący go obiekt File. Tworząc obiekt wyjściowy, jako drugi argument konstruktora, możesz przekazać wartość logicznią określającą czy zamiast zamazywać istniejący plik, dopisywać kolejne dane na jego końcu.
ByteArrayInputStream i ByteArrayOutputStream CharArrayReader i CharArrayWriter Bufor w pamięci oparty na tablicy odpowiednio bajtów lub znaków. tworząc obiekt wejściowy, przekaż konstruktorowi tablicę, na której ma być oparty. Tworząc obiekt wyjściowy przekaż konstruktorowi początkowy rozmiar bufora.
StringBufferInputStream (nie ma odpowiednika do zapisu) StringReader i StringWriter Bufor w pamięci oparty na napisie String (implementacja posługuje się obiektem StringBuffer). Tworząc obiekt wejściowy, przekaż konstruktorowi napis, na którym ma być oparty. Tworząc obiekt wyjściowy przekaż konstruktorowi początkowy rozmiar bufora. Zaleca się używanie klas z hierarchii Reader/Writer. StringBufferInputStream jest oznaczony jako deprecated.
PipedInputStream i PipedOutputStream PipedReader i PipedWriter Łącze do komunikacji między procesami. Przy pomocy konstruktora bezparametrowego należy najpierw utworzyć obiekt jednego rodzaju (wejściowy lub wyjściowy), a następnie przekazać go jako parametr konstruktora obiektu drugiego rodzaju (odpowiednio wyjściowego lub wejściowego). Strumienie zostaną połączone łączem, które będzie przesyłać dane od strumienia wyjściowego do wejściowego.

Po swoich nadklasach InputStream/OutputStream lub Reader/Writer strumienie dziedziczą podstawowe metody pozwalające odczytywać/zapisywać porcje danych. Do czytania danych ze strumienia służą metody read(). W wersji bezparametrowej zwracają wartość całkowitą reprezentującą odczytany bajt (dla InputStream) lub odczytany znak (dla Reader). Jeżeli osiągnięto koniec strumienia bezparametrowy read() zwraca -1. Przeciążone wersje metody read() odczytują dane do tablicy lub do jej części. Do wysyłania danych do strumienia służą metody write(). W podstawowej wersji przyjmują jako parametr liczbę całkowitą zawierającą zapisywany bajt (dla OutputStream) lub zapisywany znak (dla Writer). Przeciążone wersje metody write() zapisują dane z przekazanej tablicy lub jej części. Dodatkowe wersje metody z klasy Writer zapisują wszystkie znaki z przekazanego obiektu String lub jego wskazanego wycinka.

W poniższym przykładzie plik tekstowy jest odczytywany znak po znaku przy pomocy strumienia FileReader i wypisywany na standardowe wyjście.

import java.io.FileReader;
import java.io.IOException;

public class ZnakPoZnaku {
  public static void main(String[] args) throws IOException {
    // wersja dla Linuxa
    FileReader rd = new FileReader("/tmp/io_test.txt");
    // wersja dla Windows
    // FileReader rd = new FileReader("c:\\io_test.txt");
    try {
      int i;
      // Reader.read() zwraca wartość z przedziału 0 to 65535,
      // jeżeli odczyt się powiódł lub -1 jak nie
      while ((i = rd.read()) != -1)
        System.out.print((char) i);
    } finally {
      rd.close();
    }
  }
}

Po użyciu, strumień trzeba zamknąć przy pomocy metody close(). Bardzo ważne, aby o tym pamiętać szczególnie, że dla niektórych zasobów, np. dla plików dyskowych oraz połączeń sieciowych, obowiązują limity na liczbę naraz otwartych egzemplarzy. Żeby zagwarantować zwalnianie zasobów również w przypadku wystąpienia wyjątku, powinno się to robić w bloku finally. Dla zwiększenia przejrzystości W pozostałych przykładach cały kod związany z obsługą błędów i zamykaniem zasobów w blokach finally został usunięty.

Konwersja między strumieniami binarnymi i znakowymi

Strumień binarny można przekształcić na strumień znakowy. Służą do tego klasy InputStreamReader i OutputStreamWriter. Taka konwersja czasami jest bardzo przydatna, np. podczas kompresji i dekompresji danych. W poniższym przykładzie bufor oparty na tablicy bajtów jest przekształcany na obiekt Writer.

import java.io.*;

public class Test3 {
  public static void main(String[] args) throws IOException {
    String napis = "Test strumieni.\nąćęłńóśźż\n";

    ByteArrayOutputStream os = new ByteArrayOutputStream();
    // OutputStream jest przekrztałcany na Writer 
    Writer wr = new OutputStreamWriter(os);
    wr.write(napis);
    wr.close();

    ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
    // InputStream jest przekształcany na Reader 
    Reader rd = new InputStreamReader(is);
    int i;
    while ((i = rd.read()) != -1)
      System.out.print((char) i);
    rd.close();
  }
}

Kodowanie znaków

W Javie znaki są wewnętrznie kodowane w standardzie Unicode<ref name="PO_Unicode">Do wersji 1.5 każdy znak był pamiętany na dwóch bajtach i taki jest rozmiar typu char. Obecnie ze względu na rozszerzenie standardu Unicode niektóre znaki są pamiętane jako dwie wartości typu char, czyli zajmują cztery bajty. Więcej szczegółów na ten temat znajdziesz w [[1]]</ref>. Jeżeli używamy jednoparametrowego konstruktora, klasa OutputStreamWriter wykonuje konwersję na domyślne kodowanie dla danej platformy<ref name="PO_domyślne_kodownie_znaków">Maszyna wirtualna odczytuje domyślne kodowanie z ustawień systemu operacyjnego. Jego wartość jest przechowywana przez klasę System i można ją uzyskać przy pomocy wywołania System.getProperty("file.encoding").</ref>. Klasa InputStreamReader wykonuje konwersję odwrotną. Konstruktory obu klas są przeciążone i jako drugi parametr można wskazać jakiego kodowania używać zamiast domyślnego.

Domyślne kodowanie znaków jest również stosowane podczas obsługi plików za pomocą klas FileWriter i FileReader. Klasy te same nie implementują operacji na plikach. Wewnętrznie korzystają ze strumieni binarnych przekształconych przy pomocy OutputStreamWriter i InputStreamReader. Aby obsługiwać pliki przy pomocy innego niż domyślne kodowania znaków należy samemu stworzyć odpowiedni strumień binarny i przekształcić go na wersję znakową wskazując kodowanie jako drugi parametr konstruktora klas InputStreamReader bądź OutputStreamWriter.

Pobieranie danych po kolei z grupy strumieni

Klasa SequenceInputStream pozwala używać grupy strumieni InputStream jak by były skonkatenowane. Najpierw pobierane są dane z pierwszego strumienia. Gdy się skończy pobierane są dane z następnego, itd. SequenceInputStream posiada dwa konstruktory. Pierwszy przyjmuje jako parametr jedynie dwa strumienie, a drugi obiekt Enumeration<? extends InputStream>. Poniższy przykład pokazuje użycie klasy SequenceInputStream do odczytania po kolei dwóch buforów w pamięci.

import java.io.*;

public class Test2 {
  public static void main(String[] args) throws IOException {
    String daneDlaBufora1 = "Dane dla bufora 1.\nąćęłńóśźż\n";
    String daneDlaBufora2 = "Dane dla bufora 2.\nąćęłńóśźż\n";

    // getBytes() zwraca tablicę bajtów reprezentujących kolejne znakami napisu
    // w domyślnym dla danej platformy kodowaniu znaków
    ByteArrayInputStream is1 = new ByteArrayInputStream(daneDlaBufora1.getBytes());
    ByteArrayInputStream is2 = new ByteArrayInputStream(daneDlaBufora2.getBytes());
    
    SequenceInputStream seq = new SequenceInputStream(is1, is2);
    Reader rd = new InputStreamReader(seq);
    int i;
    while ((i = rd.read()) != -1)
      System.out.print((char) i);
  }
}

Informacje o kodowaniu znaków w Javie

<references/>