PO Strumienie: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
 
(Nie pokazano 24 wersji utworzonych przez 3 użytkowników)
Linia 2: Linia 2:
  
 
== System wejścia/wyjścia ==
 
== 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ć.
+
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 klas obsługi wejścia/wyjścia, chociaż początkowo ich wielość może przytłaczać.
  
 
== Strumienie ==
 
== 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 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żliwa jest przy tym bardzo łatwa konwersja strumieni binarnych na znakowe.
+
W Javie hierarchia strumieni oparta jest na czterech klasach ''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żą one 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żliwa jest przy tym bardzo łatwa konwersja strumieni binarnych na znakowe.
  
 
== Strumienie dla poszczególnych mediów ==
 
== Strumienie dla poszczególnych mediów ==
Linia 32: Linia 32:
 
</center>
 
</center>
  
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.
+
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 dają jako wynik wartość całkowitą reprezentującą odczytany bajt (dla ''InputStream'') lub odczytany znak (dla ''Reader''). Jeżeli osiągnięto koniec strumienia bezparametrowy ''read()'' daje ''-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.  
 
W poniższym przykładzie plik tekstowy jest odczytywany znak po znaku przy pomocy strumienia ''FileReader'' i wypisywany na standardowe wyjście.  
Linia 46: Linia 46:
 
     '''try''' {
 
     '''try''' {
 
       '''int''' i;
 
       '''int''' i;
       // Reader.read() zwraca wartość z przedziału 0 to 65535,
+
       // Reader.read() Daje wartość z przedziału 0 to 65535,
 
       // jeżeli odczyt się powiódł lub -1 jak nie
 
       // jeżeli odczyt się powiódł lub -1 jak nie
 
       '''while''' ((i = rd.read()) != -1)
 
       '''while''' ((i = rd.read()) != -1)
Linia 55: Linia 55:
 
   }
 
   }
 
  }
 
  }
Po użyciu, strumień trzeba zamknąć przy pomocy metody ''close()''. Trzeba 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.
+
Po użyciu, strumień trzeba zamknąć przy pomocy metody ''close()''. Trzeba 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''' lub skorzystać z instrukcji try-z-zasobami dodanej w Javie 7. 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 został usunięty.
  
 
=== Konwersja między strumieniami binarnymi i znakowymi ===
 
=== Konwersja między strumieniami binarnymi i znakowymi ===
Linia 61: Linia 61:
 
  '''import''' java.io.*;
 
  '''import''' java.io.*;
 
   
 
   
  '''public''' '''class''' Test3 {
+
  '''public''' '''class''' KonwersjaStrumieni {
 
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' IOException {
 
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' IOException {
 
     String napis = "Test strumieni.\nąćęłńóśźż\n";
 
     String napis = "Test strumieni.\nąćęłńóśźż\n";
 
   
 
   
 
     ByteArrayOutputStream os = '''new''' ByteArrayOutputStream();
 
     ByteArrayOutputStream os = '''new''' ByteArrayOutputStream();
     // OutputStream jest przekrztałcany na Writer  
+
     // OutputStream jest przekształcany na Writer  
 
     Writer wr = '''new''' OutputStreamWriter(os);
 
     Writer wr = '''new''' OutputStreamWriter(os);
 
     wr.write(napis);
 
     wr.write(napis);
Linia 90: Linia 90:
 
  '''import''' java.io.*;
 
  '''import''' java.io.*;
 
   
 
   
  '''public''' '''class''' Test2 {
+
  '''public''' '''class''' KonkatenacjaStrumieni {
 
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' IOException {
 
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' IOException {
 
     String daneDlaBufora1 = "Dane dla bufora 1.\nąćęłńóśźż\n";
 
     String daneDlaBufora1 = "Dane dla bufora 1.\nąćęłńóśźż\n";
 
     String daneDlaBufora2 = "Dane dla bufora 2.\nąćęłńóśźż\n";
 
     String daneDlaBufora2 = "Dane dla bufora 2.\nąćęłńóśźż\n";
 
   
 
   
     // getBytes() zwraca tablicę bajtów reprezentujących kolejne znakami napisu
+
     // getBytes() daje tablicę bajtów reprezentujących kolejne znaki napisu
     // w domyślnym dla danej platformy kodowaniu znaków
+
     // w domyślnym dla danej platformy kodowaniu znaków (są też wersje przeciążone)
 
     ByteArrayInputStream is1 = '''new''' ByteArrayInputStream(daneDlaBufora1.getBytes());
 
     ByteArrayInputStream is1 = '''new''' ByteArrayInputStream(daneDlaBufora1.getBytes());
 
     ByteArrayInputStream is2 = '''new''' ByteArrayInputStream(daneDlaBufora2.getBytes());
 
     ByteArrayInputStream is2 = '''new''' ByteArrayInputStream(daneDlaBufora2.getBytes());
Linia 109: Linia 109:
  
 
== Wzorzec Dekorator ==
 
== Wzorzec Dekorator ==
Omówione dotychczas klasy ukrywają szczegóły obsługi poszczególnych mediów pod abstrakcją strumienia. W każdym przypadku ze strumienia można korzystać jedynie w podstawowy sposób &ndash; odczytując lub zapisując poszczególne bajty/znaki. Przy pomocy dziedziczenie strumienie można by rozszerzyć o nową funkcjonalność, np. buforowanie danych, kompresję lub szyfrowanie. Można by też dodać nowe metody ułatwiające wykonywanie często spotykanych operacji, np. przesyłanie przez strumień innych typów danych lub przesyłanie linia po linii. Niestety potencjalna liczba kombinacji, które trzeba uwzględnić jest ogromna. Mogą być potrzebne strumienie tylko z buforowaniem, tylko z kompresją, z buforowanie i kompresją, itd. Przy zastosowaniu dziedziczenia, wszystkie kombinacje trzeba by przewidzieć zawczasu i przygotować odpowiednie podklasy. W Javie uniknięto eksplozji klas dzięki zastosowaniu wzorca Dekorator. Udostępniający podstawową funkcjonalność egzemplarz strumienia przekazuje się do obiektu "opakowującego", tak zwanego dekoratora. Obiekt dekorator implementuje ten sam interfejs lub rozszerza samą klasę bazową. Dzięki temu można go używać w zamian obiektu oryginalnego. Poszczególne metody dekoratora wywołują metody oryginalnego obiektu, a w między czasie dodają nową funkcjonalność. W odróżnieniu od dziedziczenia, nowe zachowanie dodawane jest dynamicznie, w trakcie działania programu i tylko dla pojedynczego obiektu, a nie całej klasy. Tym samym nie trzeba za wczasu przewidzieć wszystkich możliwych kombinacji. Pojedynczy obiekt może być opakowany kilkoma różnymi dekoratorami, wzbogacając go o funkcjonalność każdego z nich. Dekorator może również rozszerzać interfejs dekorowanego obiektu. W ten sposób do strumieni dodawane są metody ułatwiające wykonywanie często spotykanych operacji. Poniższy diagram pokazuje schemat hierarchii klas przy zastosowaniu wzorca Dekorator.
+
Omówione dotychczas klasy ukrywają szczegóły obsługi poszczególnych mediów pod abstrakcją strumienia. W każdym przypadku ze strumienia można korzystać jedynie w podstawowy sposób &ndash; odczytując lub zapisując poszczególne bajty/znaki. Przy pomocy dziedziczenia strumienie można by rozszerzyć o nową funkcjonalność, np. buforowanie danych, kompresję lub szyfrowanie. Można by też dodać nowe metody ułatwiające wykonywanie często spotykanych operacji, np. przesyłanie przez strumień innych typów danych lub przesyłanie linia po linii. Niestety potencjalna liczba kombinacji, które trzeba uwzględnić jest ogromna. Mogą być potrzebne strumienie tylko z buforowaniem, tylko z kompresją, z buforowaniem i kompresją, itd. Przy zastosowaniu dziedziczenia, wszystkie kombinacje trzeba by przewidzieć zawczasu i przygotować odpowiednie podklasy. W Javie uniknięto eksplozji klas dzięki zastosowaniu wzorca Dekorator. Udostępniający podstawową funkcjonalność egzemplarz strumienia przekazuje się do obiektu "opakowującego", czyli tak zwanego dekoratora. Obiekt dekorator implementuje ten sam interfejs lub rozszerza samą klasę bazową. Dzięki temu można go używać zamiast obiektu oryginalnego. Poszczególne metody dekoratora wywołują metody oryginalnego obiektu, a ponadto dodają nową funkcjonalność. W odróżnieniu od dziedziczenia, nowe zachowanie dodawane jest dynamicznie, w trakcie działania programu i tylko dla pojedynczego obiektu, a nie całej klasy. Tym samym nie trzeba zawczasu przewidzieć wszystkich możliwych kombinacji. Pojedynczy obiekt można opakować kilkoma różnymi dekoratorami, wzbogacając go o funkcjonalność każdego z nich. Dekorator może również rozszerzać interfejs dekorowanego obiektu. W ten sposób do strumieni dodawane są metody ułatwiające wykonywanie często spotykanych operacji. Poniższy diagram pokazuje schemat hierarchii klas przy zastosowaniu wzorca Dekorator.
 
<center>[[grafika:po_11_1_dekorator.png|Schemat hierarchii klas dla dekoratora]]</center>
 
<center>[[grafika:po_11_1_dekorator.png|Schemat hierarchii klas dla dekoratora]]</center>
  
Linia 118: Linia 118:
 
! strumienie binarne !! strumienie znakowe !! Opis
 
! strumienie binarne !! strumienie znakowe !! Opis
 
|-
 
|-
| ''BufferedInputStream'' i ''BufferedOutputStream'' || ''BufferedReader'' i ''BufferedWriter'' || Operacje na strumieniu stają się buforowane. W większości przypadków skutkuje to znaczącym wzrostem efektywności. Zamiast wykonywać wiele drobnych operacji na strumieniu, np. wiele razy odczytywać/zapisywać z pliku dyskowego małe porcje danych. Dekorator odczytuje większą porcję na zapas lub zapamiętuje dane, które mają być wysłane do strumienia dopóki nie uzbiera się ich dostateczna ilość. Jeżeli dane powinny być wysłane do strumienie niezwłocznie, niezależnie od tego, czy bufor jest pełen czy nie, można to wymusić wywołując metodę ''flush()''. Zazwyczaj bufor jest opróżniany również w momencie wywołania metody ''close()''.
+
| ''BufferedInputStream'' i ''BufferedOutputStream'' || ''BufferedReader'' i ''BufferedWriter'' || Operacje na strumieniu stają się buforowane. W większości przypadków skutkuje to znaczącym wzrostem efektywności. Zamiast wykonywać wiele drobnych operacji na strumieniu, np. wiele razy odczytywać/zapisywać z pliku dyskowego małe porcje danych. Dekorator odczytuje większą porcję na zapas lub zapamiętuje dane, które mają być wysłane do strumienia dopóki nie uzbiera się ich dostateczna ilość. Jeżeli dane powinny być wysłane do strumienia niezwłocznie, niezależnie od tego, czy bufor jest pełen czy nie, można to wymusić wywołując metodę ''flush()''. Zazwyczaj bufor jest opróżniany również w momencie wywołania metody ''close()''.
 
|-
 
|-
| ''DataInputStream'' i ''DataOutputStream'' || (brak odpowiedników znakowych) || Dodają wiele nowych metod pozwalających przesyłać przez strumień wartości typów podstawowych Javy oraz napisy<ref  name="modyfikacja_UTF8">Używane kodowanie jest modyfikacją UTF-8. Różnice względem standardu są opisane w [http://java.sun.com/j2se/1.5.0/docs/api/java/io/DataInput.html#modified-utf-8 dokumentacji].</ref> w sposób niezależny od platformy.
+
| ''PrintStream'' || ''PrintWriter'' || Dodają wiele nowych metod pozwalających wypisywać do strumienia dane w sposób czytelny dla człowieka. Nowe metody występują w dwóch wersjach ''print()'' i ''println()'', z których druga po ewentualnym wypisaniu przechodzi do nowego wiersza. Dodatkowo od Javay 1.5 pojawiła się również znana z C metoda ''printf()''. Dla wygody klasy mają dodatkowe konstruktory, które zwalniają z konieczności otwierania samemu strumienia do pisania do pliku i dekorowania go buforowaniem. Zalecaną klasą jest ''PrintWriter'', gdyż reprezentuje końce linii w sposób przyjęty dla używanej platformy. Uwaga: metody obu klas nie przepuszczają wyjątków ''IOException''. To czy wystąpiły trzeba kontrolować przy pomocy metody ''checkError()''.
 
|-
 
|-
| ''PrintStream'' || ''PrintWriter'' || Dodają wiele nowych metod pozwalających wypisywać do strumienia dane w sposób czytelny dla człowieka. Nowe metody występują w dwóch wersjach ''print()'' i ''println()'', z których druga na zakończenie rozpoczyna nową linię. Dla wygody klasy posiadają dodatkowe konstruktory, które zwalniają z konieczności otwierania samemu strumienia do pisania na pliku i dekorowania go buforowaniem. Zalecaną klasą jest ''PrintWriter'', gdyż reprezentuje końce linii w sposób przyjęty na danej platformie. Uwaga: metody obu klas nie przepuszczają wyjątków ''IOException''. Czy nie wystąpiły trzeba kontrolować przy pomocy metody ''checkError()''.
+
| ''PushBackInputStream'' || ''PushBackReader'' || Dodają nową metodę ''unread()'' pozwalającą odesłać z powrotem ostatnio odczytane dane. Odesłane dane są zapamiętywane w buforze. Kolejne odczyty najpierw pobierają dane z bufora, a dopiero jak jest pusty ze strumienia. Taka funkcjonalność przydaje się podczas budowy kompilatora.
 
|-
 
|-
| ''PushBackInputStream'' || ''PushBackReader'' || Dodają nową metodę ''unread()'' pozwalającą odesłać z powrotem ostatnio odczytane dane. Odesłane dane są zapamiętywane w buforze. Kolejne odczyty najpierw pobierają dane z bufora, a dopiero jak jest pusty ze strumienia. Taka funkcjonalność przydaje się podczas budowy kompilatora.
+
| ''LineNumberInputStream'' || ''LineNumberReader'' || Dodają nową metodę ''getLineNumber()'', która daje liczbę odczytanych do tej pory linii. ''LineNumberInputStream'' jest oznaczona jako deprecated, należy używać ''LineNumberReader''.
 
|-
 
|-
| ''LineNumberInputStream'' || ''LineNumberReader'' || Dodają nową metodę ''getLineNumber()'', która zwraca liczbę odczytanych do tej pory linii. ''LineNumberInputStream'' jest oznaczona jako deprecated, należy używać ''LineNumberReader''.
+
| ''DataInputStream'' i ''DataOutputStream'' || (brak odpowiedników znakowych) || Dodają wiele nowych metod pozwalających przesyłać przez strumień wartości typów podstawowych Javy oraz napisy<ref  name="modyfikacja_UTF8">Używane kodowanie jest modyfikacją UTF-8. Różnice względem standardu są opisane w [http://java.sun.com/j2se/1.5.0/docs/api/java/io/DataInput.html#modified-utf-8 dokumentacji].</ref> w sposób niezależny od platformy.
 
|-
 
|-
 
| ''ObjectInputStream'' i ''ObjectOutputStream'' || (brak odpowiedników znakowych) || Dodają nowe metody pozwalające przesyłać przez strumień obiekty implementujące interfejs ''java.io.Serializable''. Serializacja zostanie omówiona na następnym wykładzie.
 
| ''ObjectInputStream'' i ''ObjectOutputStream'' || (brak odpowiedników znakowych) || Dodają nowe metody pozwalające przesyłać przez strumień obiekty implementujące interfejs ''java.io.Serializable''. Serializacja zostanie omówiona na następnym wykładzie.
 
|-
 
|-
| ''CheckedOutputStream'' || (brak odpowiednika znakowego) || Wylicza sumę kontrolną dla danych wysyłanych do strumienia. ''CheckedOutputStream'' należy do pakietu ''java.util.zip''.
+
| ''CheckedInputStream'' i ''CheckedOutputStream'' || (brak odpowiednika znakowego) || Wylicza sumę kontrolną dla danych przesyłanych przez strumień. ''CheckedInputStream'' i ''CheckedOutputStream'' należą do pakietu ''java.util.zip''.
 
|-
 
|-
 
| ''GZIPInputStream'' i ''GZIPOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są kompresowane przy pomocy prostego algorytmu GZIP. ''GZIPInputStream'' i ''GZIPOutputStream'' należą do pakietu ''java.util.zip''.
 
| ''GZIPInputStream'' i ''GZIPOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są kompresowane przy pomocy prostego algorytmu GZIP. ''GZIPInputStream'' i ''GZIPOutputStream'' należą do pakietu ''java.util.zip''.
Linia 136: Linia 136:
 
| ''ZipInputStream'' i ''ZipOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są kompresowane przy pomocy algorytmu Zip. ''ZipInputStream'' i ''ZipOutputStream'' należą do pakietu ''java.util.zip''.
 
| ''ZipInputStream'' i ''ZipOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są kompresowane przy pomocy algorytmu Zip. ''ZipInputStream'' i ''ZipOutputStream'' należą do pakietu ''java.util.zip''.
 
|-
 
|-
| ''CipherInputStream'' i ''CipherOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są szyfrowane lub deszyfrowane przy pomocy obiektu klasy ''Cipher''. ''CipherInputStream'', ''CipherOutputStream'' i ''Cipher'' należą do pakietu ''javax.Crypto''.
+
| ''CipherInputStream'' i ''CipherOutputStream'' || (brak odpowiedników znakowych) || Dane przesyłane przez strumień są szyfrowane lub deszyfrowane przy pomocy obiektu klasy ''Cipher''. ''CipherInputStream'', ''CipherOutputStream'' i ''Cipher'' należą do pakietu ''javax.crypto''.
 
|}
 
|}
 
</center>
 
</center>
Linia 172: Linia 172:
 
  }
 
  }
 
Połączenie ''DataOutputStream'', ''BufferedOutputStream'' i ''FileOutputStream'' wyjaśnia poniższa animacja, w której dla uproszczenia bufor ma rozmiar 10 bajtów. W rzeczywistości, jeżeli sami nie wskażemy innej wielkości, bufor będzie miał 8192 bajty.
 
Połączenie ''DataOutputStream'', ''BufferedOutputStream'' i ''FileOutputStream'' wyjaśnia poniższa animacja, w której dla uproszczenia bufor ma rozmiar 10 bajtów. W rzeczywistości, jeżeli sami nie wskażemy innej wielkości, bufor będzie miał 8192 bajty.
<div class="thumb tnone"><div style="width:473px;"><center><flash>file=PO_strumienie_film.swf|width=465|height=380</flash><div class="thumbcaption">Tytuł animacji</div></center></div></div>
+
<div class="thumb tnone"><div style="width:559px;"><center><flash>file=PO_strumienie_film.swf|width=550|height=449</flash><div class="thumbcaption">Zapisywanie różnych typów danych do pliku z buforowaniem</div></center></div></div>
  
 
== Standardowe wejście/wyjście ==
 
== Standardowe wejście/wyjście ==
 
Pomysł, aby dane wejściowe dla programu były odczytywane z jednego strumienia &ndash; '''standardowego wejścia''' (''ang. standard input''), dane wyjściowe były wysyłane do '''standardowego wyjścia''' (''ang. standard output''), a informacje o błędach do '''standardowego wyjścia błędu''' (''ang. standard error'') pochodzi z systemów Unixowych. Dzięki temu staje się możliwe łączenia programów w potoki przetwarzania &ndash; standardowe wyjście jednego programu staje się standardowym wyjściem drugiego, itd. Ten pomysł został zaadoptowany w wielu innych systemach operacyjnych, m.in. również w Windows.
 
Pomysł, aby dane wejściowe dla programu były odczytywane z jednego strumienia &ndash; '''standardowego wejścia''' (''ang. standard input''), dane wyjściowe były wysyłane do '''standardowego wyjścia''' (''ang. standard output''), a informacje o błędach do '''standardowego wyjścia błędu''' (''ang. standard error'') pochodzi z systemów Unixowych. Dzięki temu staje się możliwe łączenia programów w potoki przetwarzania &ndash; standardowe wyjście jednego programu staje się standardowym wyjściem drugiego, itd. Ten pomysł został zaadoptowany w wielu innych systemach operacyjnych, m.in. również w Windows.
  
W Javie do standardowego wyjścia/wejścia mamy dostęp poprzez zmienne statyczne klasy ''System''. Na ''System.out'' i ''System.err'' przypisane są obiekt ''PrintStream''. ''System.in'' to zwykły ''InputStream''. Najczęściej ze standardowego wejścia będziemy wczytywać dane tekstowe, np. komendy od użytkownika, dlatego warto standardowe wejście opakować w ''BufferedReader'' i używać jego metody ''readLine()''.
+
W Javie do standardowego wyjścia/wejścia mamy dostęp poprzez zmienne statyczne klasy ''System''. Na ''System.out'' i ''System.err'' przypisane są obiekt ''PrintStream''. ''System.in'' to zwykły ''InputStream''. Najczęściej ze standardowego wejścia będziemy wczytywać dane tekstowe, np. polecenia od użytkownika, dlatego warto standardowe wejście opakować w ''BufferedReader'' i używać jego metody ''readLine()''.
 
  BufferedReader stin = new BufferedReader(new InputStreamReader(System.in));
 
  BufferedReader stin = new BufferedReader(new InputStreamReader(System.in));
  
 
=== Przekierowywanie standardowego wejścia/wyjścia ===
 
=== Przekierowywanie standardowego wejścia/wyjścia ===
Możliwe jest przekierowanie standardowego wyjścia, wejścia i wejścia błędu. Słąż do tego metody ''setOut(PrintStream)'', ''setIn(InputStream)'' i ''setErr(PrintStream)''. Może to, np. ułatwić testowanie aplikacji. Sekwencje komend użytkownika wystarczy zapisać w pliku i przekierować standardowe wejście na strumień go odczytujący. Standardowe wyjście można przekierować do innego pliku, żeby móc sprawdzić czy jest zgodne z oczekiwaniami.
+
Możliwe jest przekierowanie standardowego wyjścia, wejścia i wyjścia dla komunikatach o błędach. Służą do tego metody ''setOut(PrintStream)'', ''setIn(InputStream)'' i ''setErr(PrintStream)''. Może to, np. ułatwić testowanie aplikacji. Sekwencje poleceń użytkownika wystarczy zapisać w pliku i przekierować standardowe wejście na strumień go odczytujący. Standardowe wyjście można przekierować do pliku, żeby łatwiej móc sprawdzić, czy wyniki są zgodne z oczekiwaniami.
  
 
== Kompresja ==
 
== Kompresja ==
Linia 259: Linia 259:
  
 
== Nowe wejście/wyjście ==
 
== Nowe wejście/wyjście ==
W Javie 1.4 dodano "nowe" biblioteki wejścia/wyjścia (''ang. new I/O''). Są one zebrane w pakietach ''java.nio.*''. Szczegółowe omówienie tych bibliotek wykracza poza zakres naszego wykładu. Głównym celem przy opracowywaniu nowych bibliotek był wzrost prędkości działania. Z tego powodu przyjęte rozwiązania są bliskie do sposobu organizacji wejścia/wyjścia w systemach operacyjnych. Posługiwanie się nowymi bibliotekami jest bardziej pracochłonne i wymaga więcej uwagi. Jeżeli nie zależy nam osiągnięciu maksymalnej możliwej prędkości działania wejścia/wyjścia, dotychczasowe "stare" biblioteki są nadal zalecana. Ich efektywność również uległa poprawieniu dzięki przepisaniu ich kodu przy pomocy nowych bibliotek.
+
W Javie 1.4 dodano "nowe" biblioteki wejścia/wyjścia (''ang. new I/O''). Są one zebrane w pakietach ''java.nio.*''. Szczegółowe omówienie tych bibliotek wykracza poza zakres naszego wykładu. Głównym celem przy opracowywaniu nowych bibliotek był wzrost prędkości działania. Z tego powodu przyjęte rozwiązania są bliskie do sposobu organizacji wejścia/wyjścia w systemach operacyjnych. Posługiwanie się nowymi bibliotekami jest bardziej pracochłonne i wymaga więcej uwagi. Jeżeli nie zależy nam osiągnięciu maksymalnej możliwej prędkości działania wejścia/wyjścia, dotychczasowe "stare" biblioteki są nadal zalecane. Ich efektywność również uległa poprawieniu dzięki przepisaniu ich kodu przy pomocy nowych bibliotek.
  
 
== Informacje o kodowaniu znaków w Javie ==
 
== Informacje o kodowaniu znaków w Javie ==
 
<references/>
 
<references/>

Aktualna wersja na dzień 00:15, 9 maj 2012

<<< Powrót do przedmiotu Programowanie obiektowe

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 klas obsługi wejścia/wyjścia, chociaż początkowo ich wielość 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 na czterech klasach 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żą one 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żliwa 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 dają jako wynik wartość całkowitą reprezentującą odczytany bajt (dla InputStream) lub odczytany znak (dla Reader). Jeżeli osiągnięto koniec strumienia bezparametrowy read() daje -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() Daje 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(). Trzeba 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 lub skorzystać z instrukcji try-z-zasobami dodanej w Javie 7. 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 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 KonwersjaStrumieni {
  public static void main(String[] args) throws IOException {
    String napis = "Test strumieni.\nąćęłńóśźż\n";

    ByteArrayOutputStream os = new ByteArrayOutputStream();
    // OutputStream jest przekształ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 Javy 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 dokumentacji</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 tak, jakby były skonkatenowane. Najpierw pobierane są dane z pierwszego strumienia. Gdy ten 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 KonkatenacjaStrumieni {
  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() daje tablicę bajtów reprezentujących kolejne znaki napisu
    // w domyślnym dla danej platformy kodowaniu znaków (są też wersje przeciążone)
    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);
  }
}

Wzorzec Dekorator

Omówione dotychczas klasy ukrywają szczegóły obsługi poszczególnych mediów pod abstrakcją strumienia. W każdym przypadku ze strumienia można korzystać jedynie w podstawowy sposób – odczytując lub zapisując poszczególne bajty/znaki. Przy pomocy dziedziczenia strumienie można by rozszerzyć o nową funkcjonalność, np. buforowanie danych, kompresję lub szyfrowanie. Można by też dodać nowe metody ułatwiające wykonywanie często spotykanych operacji, np. przesyłanie przez strumień innych typów danych lub przesyłanie linia po linii. Niestety potencjalna liczba kombinacji, które trzeba uwzględnić jest ogromna. Mogą być potrzebne strumienie tylko z buforowaniem, tylko z kompresją, z buforowaniem i kompresją, itd. Przy zastosowaniu dziedziczenia, wszystkie kombinacje trzeba by przewidzieć zawczasu i przygotować odpowiednie podklasy. W Javie uniknięto eksplozji klas dzięki zastosowaniu wzorca Dekorator. Udostępniający podstawową funkcjonalność egzemplarz strumienia przekazuje się do obiektu "opakowującego", czyli tak zwanego dekoratora. Obiekt dekorator implementuje ten sam interfejs lub rozszerza tę samą klasę bazową. Dzięki temu można go używać zamiast obiektu oryginalnego. Poszczególne metody dekoratora wywołują metody oryginalnego obiektu, a ponadto dodają nową funkcjonalność. W odróżnieniu od dziedziczenia, nowe zachowanie dodawane jest dynamicznie, w trakcie działania programu i tylko dla pojedynczego obiektu, a nie całej klasy. Tym samym nie trzeba zawczasu przewidzieć wszystkich możliwych kombinacji. Pojedynczy obiekt można opakować kilkoma różnymi dekoratorami, wzbogacając go o funkcjonalność każdego z nich. Dekorator może również rozszerzać interfejs dekorowanego obiektu. W ten sposób do strumieni dodawane są metody ułatwiające wykonywanie często spotykanych operacji. Poniższy diagram pokazuje schemat hierarchii klas przy zastosowaniu wzorca Dekorator.

Schemat hierarchii klas dla dekoratora

Niektóre klasy ze standardowych bibliotek Javy rozszerzające funkcjonalność strumieni są wymienione w tabeli.

Niektóre klasy ze standardowych bibliotek Javy rozszerzające funkcjonalność strumieni
strumienie binarne strumienie znakowe Opis
BufferedInputStream i BufferedOutputStream BufferedReader i BufferedWriter Operacje na strumieniu stają się buforowane. W większości przypadków skutkuje to znaczącym wzrostem efektywności. Zamiast wykonywać wiele drobnych operacji na strumieniu, np. wiele razy odczytywać/zapisywać z pliku dyskowego małe porcje danych. Dekorator odczytuje większą porcję na zapas lub zapamiętuje dane, które mają być wysłane do strumienia dopóki nie uzbiera się ich dostateczna ilość. Jeżeli dane powinny być wysłane do strumienia niezwłocznie, niezależnie od tego, czy bufor jest pełen czy nie, można to wymusić wywołując metodę flush(). Zazwyczaj bufor jest opróżniany również w momencie wywołania metody close().
PrintStream PrintWriter Dodają wiele nowych metod pozwalających wypisywać do strumienia dane w sposób czytelny dla człowieka. Nowe metody występują w dwóch wersjach print() i println(), z których druga po ewentualnym wypisaniu przechodzi do nowego wiersza. Dodatkowo od Javay 1.5 pojawiła się również znana z C metoda printf(). Dla wygody klasy mają dodatkowe konstruktory, które zwalniają z konieczności otwierania samemu strumienia do pisania do pliku i dekorowania go buforowaniem. Zalecaną klasą jest PrintWriter, gdyż reprezentuje końce linii w sposób przyjęty dla używanej platformy. Uwaga: metody obu klas nie przepuszczają wyjątków IOException. To czy wystąpiły trzeba kontrolować przy pomocy metody checkError().
PushBackInputStream PushBackReader Dodają nową metodę unread() pozwalającą odesłać z powrotem ostatnio odczytane dane. Odesłane dane są zapamiętywane w buforze. Kolejne odczyty najpierw pobierają dane z bufora, a dopiero jak jest pusty ze strumienia. Taka funkcjonalność przydaje się podczas budowy kompilatora.
LineNumberInputStream LineNumberReader Dodają nową metodę getLineNumber(), która daje liczbę odczytanych do tej pory linii. LineNumberInputStream jest oznaczona jako deprecated, należy używać LineNumberReader.
DataInputStream i DataOutputStream (brak odpowiedników znakowych) Dodają wiele nowych metod pozwalających przesyłać przez strumień wartości typów podstawowych Javy oraz napisy<ref name="modyfikacja_UTF8">Używane kodowanie jest modyfikacją UTF-8. Różnice względem standardu są opisane w dokumentacji.</ref> w sposób niezależny od platformy.
ObjectInputStream i ObjectOutputStream (brak odpowiedników znakowych) Dodają nowe metody pozwalające przesyłać przez strumień obiekty implementujące interfejs java.io.Serializable. Serializacja zostanie omówiona na następnym wykładzie.
CheckedInputStream i CheckedOutputStream (brak odpowiednika znakowego) Wylicza sumę kontrolną dla danych przesyłanych przez strumień. CheckedInputStream i CheckedOutputStream należą do pakietu java.util.zip.
GZIPInputStream i GZIPOutputStream (brak odpowiedników znakowych) Dane przesyłane przez strumień są kompresowane przy pomocy prostego algorytmu GZIP. GZIPInputStream i GZIPOutputStream należą do pakietu java.util.zip.
ZipInputStream i ZipOutputStream (brak odpowiedników znakowych) Dane przesyłane przez strumień są kompresowane przy pomocy algorytmu Zip. ZipInputStream i ZipOutputStream należą do pakietu java.util.zip.
CipherInputStream i CipherOutputStream (brak odpowiedników znakowych) Dane przesyłane przez strumień są szyfrowane lub deszyfrowane przy pomocy obiektu klasy Cipher. CipherInputStream, CipherOutputStream i Cipher należą do pakietu javax.crypto.

W poniższym przykładzie plik wypełniany jest różnymi danymi przy pomocy DataOutputStream, a następnie odczytywany przy pomocy DataInputStream. Dla przyspieszenia operacje na pliku są dodatkowo buforowane.

import java.io.*;

public class TestDekoratorów {
  public static void main(String[] args) throws IOException {
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/io_test.txt";
    // wersja dla Windows
    //String nazwaPliku = "c:\\io_test.txt";

    DataOutputStream os = new DataOutputStream(
                            new BufferedOutputStream (
                              new FileOutputStream(nazwaPliku)));
    os.writeBoolean(true);
    os.writeInt(1234567890);
    os.writeDouble(Math.PI);
    os.writeUTF("Test strumieni.\nąćęłńóśźż\n");
    os.close();// wykonuje flush()
   
    DataInputStream is = new DataInputStream(
                           new BufferedInputStream (
                             new FileInputStream(nazwaPliku)));
    Boolean b = is.readBoolean();
    Integer i = is.readInt();
    Double d = is.readDouble();
    String str = is.readUTF();
    is.close();
   
    System.out.printf("Odczytano: b=%s, i=%s, d=%s, str=%s", b, i, d, str);
  }  
}

Połączenie DataOutputStream, BufferedOutputStream i FileOutputStream wyjaśnia poniższa animacja, w której dla uproszczenia bufor ma rozmiar 10 bajtów. W rzeczywistości, jeżeli sami nie wskażemy innej wielkości, bufor będzie miał 8192 bajty.

<flash>file=PO_strumienie_film.swf|width=550|height=449</flash>
Zapisywanie różnych typów danych do pliku z buforowaniem

Standardowe wejście/wyjście

Pomysł, aby dane wejściowe dla programu były odczytywane z jednego strumienia – standardowego wejścia (ang. standard input), dane wyjściowe były wysyłane do standardowego wyjścia (ang. standard output), a informacje o błędach do standardowego wyjścia błędu (ang. standard error) pochodzi z systemów Unixowych. Dzięki temu staje się możliwe łączenia programów w potoki przetwarzania – standardowe wyjście jednego programu staje się standardowym wyjściem drugiego, itd. Ten pomysł został zaadoptowany w wielu innych systemach operacyjnych, m.in. również w Windows.

W Javie do standardowego wyjścia/wejścia mamy dostęp poprzez zmienne statyczne klasy System. Na System.out i System.err przypisane są obiekt PrintStream. System.in to zwykły InputStream. Najczęściej ze standardowego wejścia będziemy wczytywać dane tekstowe, np. polecenia od użytkownika, dlatego warto standardowe wejście opakować w BufferedReader i używać jego metody readLine().

BufferedReader stin = new BufferedReader(new InputStreamReader(System.in));

Przekierowywanie standardowego wejścia/wyjścia

Możliwe jest przekierowanie standardowego wyjścia, wejścia i wyjścia dla komunikatach o błędach. Służą do tego metody setOut(PrintStream), setIn(InputStream) i setErr(PrintStream). Może to, np. ułatwić testowanie aplikacji. Sekwencje poleceń użytkownika wystarczy zapisać w pliku i przekierować standardowe wejście na strumień go odczytujący. Standardowe wyjście można przekierować do pliku, żeby łatwiej móc sprawdzić, czy wyniki są zgodne z oczekiwaniami.

Kompresja

Dzięki gotowym dekoratorom z pakietu java.util.zip kompresja w Javie jest bardzo prosta. W poniższym przykładzie dane zapisywane do pliku są kompresowane przy pomocy prostego algorytmu GZIP.

import java.io.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class KompresjaGZIP {
  public static void main(String[] args) throws IOException {
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/io_test.gzip";
    // wersja dla Windows
    //String nazwaPliku = "c:\\io_test.gzip";

    BufferedOutputStream os = new BufferedOutputStream(
                                new GZIPOutputStream(
                                  new FileOutputStream(nazwaPliku)));
    PrintWriter pw = new PrintWriter(
                       new OutputStreamWriter(os));
    pw.write("kompresja w Javie jest prosta");
    pw.close();
    
    // dekompresja
    BufferedInputStream is = new BufferedInputStream(
                               new GZIPInputStream(
                                 new FileInputStream(nazwaPliku)));
    BufferedReader br = new BufferedReader(
                          new InputStreamReader(is));
    String s;
    while ( (s = br.readLine()) != null )
      System.out.println(s);
    br.close();
  }
}

Format zip daje większe możliwości. W jednym archiwum można skompresować wiele plików. Kolejne elementy archiwum reprezentowane są przez obiekty ZipEntry. Poniższy przykład prezentuje tworzenie archiwum składającego się z wielu plików. Jako pierwszy parametr należy przekazać ścieżkę do katalogu, w którym znajduja sie pliki do skompresowania, a następnie ich nazwy. Kompresowane pliki mogą znajdować się w podkatalogach. Przykładowe wywołanie programu w systemi Linux może wyglądać następujaco:

java KompresjaZip /home/mójUżytkownik/ a.txt b.txt podkat\c.txt podkat\d.txt
import java.io.*;
import java.util.zip.*;

public class KompresjaZip {
  public static void main(String[] args) throws IOException {
    // wersja dla Linuxa
    String nazwaPliku = "/tmp/io_test.zip";
    // wersja dla Windows
    //String nazwaPliku = "c:\\io_test.zip";
    int x;

    ZipOutputStream zos = new ZipOutputStream(
                            new BufferedOutputStream(
                              new FileOutputStream(nazwaPliku)));
    for (int i = 1; i < args.length; i++) {
      System.out.println("Zapisywanie " + args[i]);
      BufferedInputStream in = new BufferedInputStream(new FileInputStream(args[0]+args[i]));
      zos.putNextEntry(new ZipEntry(args[i]));
      while ( (x = in.read()) != -1 )
        zos.write(x);
      in.close();
    }
    zos.close();
    
    ZipInputStream zis = new ZipInputStream(
                           new BufferedInputStream(
                             new FileInputStream(nazwaPliku)));
    ZipEntry ze;
    while ( (ze = zis.getNextEntry()) != null ) {
      System.out.println();
      System.out.println("Odczytywanie: " + ze);
      while ( (x = zis.read()) != -1 )
        System.out.write(x);
    }
    System.out.flush();
    zis.close();
  }
}

Nowe wejście/wyjście

W Javie 1.4 dodano "nowe" biblioteki wejścia/wyjścia (ang. new I/O). Są one zebrane w pakietach java.nio.*. Szczegółowe omówienie tych bibliotek wykracza poza zakres naszego wykładu. Głównym celem przy opracowywaniu nowych bibliotek był wzrost prędkości działania. Z tego powodu przyjęte rozwiązania są bliskie do sposobu organizacji wejścia/wyjścia w systemach operacyjnych. Posługiwanie się nowymi bibliotekami jest bardziej pracochłonne i wymaga więcej uwagi. Jeżeli nie zależy nam osiągnięciu maksymalnej możliwej prędkości działania wejścia/wyjścia, dotychczasowe "stare" biblioteki są nadal zalecane. Ich efektywność również uległa poprawieniu dzięki przepisaniu ich kodu przy pomocy nowych bibliotek.

Informacje o kodowaniu znaków w Javie

<references/>