STRUMIENIE

AGENDA


 
        1. Koncepcja strumienia
        2. Klasy strumieniowe w Javie
        3. Klasy przedmiotowe
        4. Wyjątki (na marginesie)
        5. Klasy przetwarzające
        6. Kodowanie
        7. Strumienie z Internetu
        8. Serializacja
        9. Inne klasy we-wy


KONCEPCJA STRUMIENIA



KLASY STRUMIENIOWE

W Javie do obsługi strumieni służą klasy z pakietu java.io

Klasy te można podzielić na grupy wg następujących kryteriów:

Na poniższych rysunkach pokazano hierarchię klas znakowych i strumieniowych.
Zaciemnione elementy oznaczają klasy przedmiotowe (związane z konkretnym źródłem/odbiornikiem), jasne - klasy przetwrazające (realizujące określone rodzaje orzetwarzania).

Klasy dla strumieni bajtowych

Żródło: Java Tutorial, JavaSoft
 

Klasy dla strumieni znakowych


Żródło: Java Tutorial, JavaSoft
 

UWAGA: Przy przetwarzaniu tekstowym należy korzystać ze strumieni znakowych, bowiem tylko one zapewniają poprawne przetwarzanie unikodu.

Nadklasy, z których wywodzą się wszystkie inne klasy strumieniowe (czyli nadklasy InputStream, OutputStream, Reader, Writer) są abstrakcyjne i zawierają deklaracje podstawowych metod przetwarzania strumieni, które podklasy winny implementować.


KLASY PRZEDMIOTOWE
(związane z konkretnym źródłem/odbiornikiem)



 
 

Źródło/odbiornik Strumienie znakowe Strumienie bajtowe
Pamięć CharArrayReader,
CharArrayWriter
ByteArrayInputStream,
ByteArrayOutputStream
StringReader,
StringWriter
StringBufferInputStream
Potok PipedReader,
PipedWriter
PipedInputStream,
PipedOutputStream
Plik FileReader,
FileWriter
FileInputStream,
FileOutputStream
Źródło: Java Tutorial

Przykład:
kopiowanie plików bajt po bajcie.

import java.io.*;

public class Copy1 {

public static void main(String[] args) {
 

 if (args.length != 2) {
    System.out.println("Syntax: in out");
    System.exit(1);
    }

 FileInputStream in  = null;
 FileOutputStream out = null;

 try {
   in  = new FileInputStream(args[0]);
   out = new FileOutputStream(args[1]);
   int c = 0;
   while ((c = in.read()) != -1) out.write(c);

 } catch(FileNotFoundException exc) {
   System.out.println("Plik " + args[0] + " nie istnieje.");
   System.exit(1);

 } catch(IOException exc) {
   System.out.println(exc.getMessage());
   System.exit(1);

 } finally {
   try {
     if (in != null) in.close();
     if (out != null) out.close();
   } catch (IOException exc) { System.out.println(""+exc); }

 }

}

Przy tej okazji omówimy wyjątki.



KLASY PRZETWARZAJĄCE
(implementujące "abstrakcyjne" przetwarzanie)


Rodzaj przetwarzania  Strumienie znakowe Strumienie bajtowe
Buforowanie BufferedReader,
BufferedWriter
BufferedInputStream,
BufferedOutputStream
Filtrowanie FilterReader,
FilterWriter
FilterInputStream,
FilterOutputStream
Konwersja: bajty-znaki InputStreamReader,
OutputStreamWriter
 
Konkatenacja   SequenceInputStream
Serializacja obiektów   ObjectInputStream,
ObjectOutputStream
Konwersje danych   DataInputStream,
DataOutputStream
Zliczanie wierszy LineNumberReader LineNumberInputStream
Podglądanie PushbackReader PushbackInputStream
Drukowanie PrintWriter PrintStream
Żródło: Java Tutorial

Komentarze:

Przykład:
przy czytaniu dużych plików tekstowych należy unikać bezpośredniego czytania za pomocą klasy FileReader, bowiem każde odczytanie znaku powoduje fizyczne odwołanie do pliku (to samo dotyczy zapisu i klasy FileWriter).
Można zastosować klasę BufferedReader (buforowanie)
Ale klasa ta jest klasą przetwarzającą, wobec tego nie pozwala bezpośrednio podać fizycznego źródła danych.
Żródło to podajemy przy konstrukcji obiektu typu FileReader, a po to, żeby uzyskać buforowanie, "opakowujemy" FileReadera BufferedReaderem.
Wygląda to tak:

FileReader fr = new FileReader("plik.txt");        // tu powstaje związek z fizycznym źródłem
BufferedReader br = new BufferedReader(fr); // tu dodajemy "opakowanie", umożliwiające buforowanie
//...  teraz wszelkie odwołania czytania itp. kierujemy do obiektu br

Przykładowy program: stworzyć metodę umożliwiającą określenie czy w pliku podanym jako argument występuje którekolwiek z podanych słów.

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 stwierdzenie, czy w wierszu występuje któreś ze słów
        }
      br.close();  // zamknięcie strumienia
      }
    catch (IOException e) {
      System.err.println(e);
    }

Przy okazji tego programu użyjemy  trzech  ważnych klas Javy:

Klasa pozwalająca na przeszukiwanie pliku ma następującą postać

public class Search {

 public boolean hasAnyWord(String fname, Hashtable wordtab) {

  boolean result = false;
  try {
      String line;
      FileReader fr = new FileReader(fname);
      BufferedReader br = new BufferedReader(fr);
      search:
      while  ((line = br.readLine()) != null) {
        StringTokenizer st = new StringTokenizer(line, " ,.:;()\t\r\n");
        while (st.hasMoreTokens()) {
           String word = st.nextToken();
           if (wordtab.get(word) != null) {
             result = true;
             break search;
             }
           }
        }
      br.close();
      }
    catch (IOException e) {
      System.err.println(e);
    }
 return result;
 }
}

Jest tu jedna metoda - hasAnyWord - stwierdzająca, czy plik podany jako jej pierwszy argument zawiera którekolwiek ze słów, zapisanych w tablicy asocjacyjnej wordtab. Użycie tej tablicy przyspiesza wyszukiwanie, bowiem słów może być dużo, a gdy są one zapisane jako klucze (jakichś) wartości w tablicy asocjacyjnej, to stwierdzenie czy jakieś słowo w niej występuje jest błyskawiczne: odwołanie wordtab.get(word) (wynik jest null jeśli nie ma, jeśli jest dostajemy Object skojarzony z danym kluczem; tu nieważny).
A co to są słowa? Tu traktujemy je jako ciągi znaków nie zawierające " ,.:;()\t\r\n" i używamy StringTokenizera do ich wyodrębnienia (konstruktor StringTokenizer ma za argumentu String na którym ma dzialać oraz ograniczniki definiujące jakie symbole będą wyodrębniane).

Testująca klasa ma następującą postać:

public class Test {

   public static void main(String[] args) {  // argumenty: nazwa_pliku slowo1 slowo2 ... slowoN
     if (args.length < 2) System.exit(1);
     Object dummy = new Object();
     Hashtable words = new Hashtable();

     for (int i = 1; i<args.length; i++) words.put(args[i], dummy);  // wartość nas nie obchodzi, tylko klucz

     Search srch = new Search();
     boolean result = srch.hasAnyWord(args[0], words);

     String msg = " nie zawiera żadnego ze słów:";
     if (result) msg = " zawiera któreś ze słów:";

     System.out.println("Plik "+args[0]+msg);
     Enumeration en = words.keys();                   // uzyskujemy wszystkie klucze tablicy
     while (en.hasMoreElements()) {                   // ... i przebiegamy je po kolei
       String word = (String) en.nextElement();
       System.out.println(word);
       }
   }
}
 


KODOWANIE

Java posługuje się znakami w formacie Unicode (UTF8). Są to - ogólnie - wielkości 16-bitowe.
Środowiska natywne (np. Windows) najczęściej zapisują teksty jako sekwencje bajtów (z przyjętą stroną kodową).
Jak pogodzić najczęściej bajtowy charakter plików natywnych ze znakowymi strumieniami?
Otóż strumienie znakowe potrafią - niewidocznie dla nas -  przekształcać bajtowe źródła w znakowe strumienie i odwrotnie. "Pod pokrywką" tego procesu znajdują się dwie klasy: InputStreamReader i OutputStreamWriter, które dokonują właściwych konwersji w trakcie czytania/pisania.
Klasy te możemy wykorzystać również samodzielnie.
Jeśli w konstruktorach tych klas nie podamy enkodowania przy konwersjach zostanie przyjęte domyślne.
Aby się dowiedzieć, jakie jest domyślne enkodowanie można użyć następującego programiku:

public class DefaultEncoding {
      public static void main(String args[])
      {
        String p = System.getProperty("file.encoding");
        System.out.println(p);
        }
}

W zależności od ustawień na danej platformie otrzymamy różne wyniki. Np. ibm-852 lub Cp852 (Latin 2) albo Cp1252 (Windows Western Europe / Latin-1).

Inna wersja konstruktorów pozwala na podanie enkodowania.
Napiszmy program wykonujący konwersje plików z-do dowolnych (dopuszczalnych przez Javę) formatów kodowania.
Dopuszczalne symbole enkodowania można znaleźć na stronie:
http://java.sun.com/products/jdk/1.1/docs/guide/intl/encoding.doc.html

import java.io.*;
 

public class Convert {

public static void main(String[] args) {

if (args.length != 4) {
   System.out.println("Syntax: in in_enc out out_enc");
   System.exit(1);
   }

String infile  = args[0],     // plik wejściowy
       in_enc  = args[1],      // wejściowa strona kodowa
       outfile = args[2],       // plik wyjściowy
       out_enc = args[3];    // wyjściowa strona kodowa

try {
 FileInputStream fis = new FileInputStream(infile);
 BufferedReader in = new BufferedReader(new InputStreamReader(fis, in_enc));
 FileOutputStream fos = new FileOutputStream(outfile);
 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos, out_enc));
 String line;
 while ((line = in.readLine()) != null) {
         out.write(line);
         out.newLine();                               // zapis znaku końca wiersza
        }
 in.close();
 out.close();
 }
 catch (IOException e) {
         System.err.println(e);
 }

}

Przykładowe wykorzystanie do konwersji pliku zle.htm (zapisanego w Windows 1250) na plik dobrze.htm ( ISO-8859-2):

 java Convert zle.htm Cp1250 dobrze.htm ISO8859_2
 
 



STRUMIENIE Z INTERNETU

Abstrakcyjny strumień (np. InputStream) może być związany z zasobem sieci, oznaczanym przez URL.
To co dostaniemy w rezultacie czytania takiego strumienia zależy od tego w jaki sposób serwer definiuje przesyłanie informacji dotyczących tego zasobu.

Dwa sposoby działania:
 


Przykład - czytanie  z Internetu dokumentów html, których adresy (np. w postaci: http://....) zapisane są w pliku podanym jako argument programu.

import java.net.*;        // konieczne do posługiwania się klasą URL
import java.io.*;
import java.util.*;

public class URLReader {

 public static void main(String[] args) throws Exception {
   BufferedReader list = new BufferedReader(
                             new FileReader(args[0]));
   String urlString;
   while ((urlString = list.readLine()) != null) {
      System.out.println("Trying to read "+ urlString);
      readAndSave(new URL(urlString));                        // tworzony nowy obiekt klasy URL
      }
   list.close();
   System.exit(0);

 }
 

 static void readAndSave(URL url) throws Exception {

   BufferedReader in = new BufferedReader(
                         new InputStreamReader(url.openStream()));       // url.openStream() zwraca InputStream

   String fname = null;
   StringTokenizer st = new StringTokenizer(url.getFile(), "/");
   while (st.hasMoreTokens()) fname = st.nextToken();                  // pobieramy nazwę pliku pod którą ma być zachowany

   BufferedWriter out = new BufferedWriter(new FileWriter(fname));

   String s;
   while ((s = in.readLine()) != null) {
         out.write(s);
         out.newLine();
         }
   in.close();
   out.close();
   }

}
 


SERIALIZACJA (SZEREGOWANIE)

Serializacja polega na zapisywaniu obiektów do strumienia.
Zapisywany jest pełny stan obiektu (w tym - rekursywnie - obiektów składowych).
Nie są zapisywane pola statyczne oraz pola deklarowane z identyfikatorem transient.

Wykorzystanie:


Do zapisywania/odczytywania obiektów służą klasy ObjectOutputStream oraz ObjectInputStream.

Przykład: tablica asocjacyjna kolorów, umożliwiająca odwoływanie się do kolorów przez nazwy.

import java.io.*;
import java.awt.*;
import java.util.*;

public class Kolory {

   public static void main(String[] args) {
      Hashtable c = new Hashtable();
      c.put("black", Color.black);
      c.put("blue", Color.blue);
      c.put("red", Color.red);
      c.put("yellow", Color.yellow);
      c.put("white", Color.white);
      try {
       FileOutputStream out = new FileOutputStream("KOLORY.SER");
       ObjectOutputStream s = new ObjectOutputStream(out);
       s.writeObject(c);
       s.flush();
       }
       catch(IOException exc) { System.out.println(exc); }

   }
 

W jakiejkolwiek innej aplikacji możemy teraz wybrać kolor z zapisanej tablicy, używając jego nazwy np.:

FileInputStream in = new FileInputStream("KOLORY.SER");
ObjectInputStream ois = new ObjectInputStream(in);
Hashtable ht = (Hashtable) ois.readObject();  // zapamiętany obiekt Hashtable - odtworzony
 Color c = (Color) ht.get(nazwa_koloru);
 

Aby obiekt jakiejś klasy mógł być zapisany do strumienia, klasa ta musi implementować interfejs Serializable.
Standardowe klasy Javy implementują ten interfejs.
W naszych klasach musimy zapewnić tę implementację (co nie jest trudne, bowiem interfejs jest pusty).

Przykład:
Serializowalna klasa Slist stanowi okno z listą i przyciskiem "Save", który pozwala zapamiętać bieżący stan obiektu (zawartość listy, kolory, pismo etc.) w pliku SLIST.SER.

import java.io.*;
import java.awt.*;
import java.util.*;

class Slist extends Frame implements Serializable, ActionListener {

  List list = new List();
  Button b = new Button("Save");

  Slist() {
    b.addActionListener(this);
    add(list, "Center");
    add(b, "South");
  }

  public List getList() { return list; }

  public void actionPerformed(ActionEvent e)  {
   try {
    FileOutputStream out = new FileOutputStream("SLIST.SER");
    ObjectOutputStream s = new ObjectOutputStream(out);
    s.writeObject(this);
    s.flush();
    }
     catch(IOException exc) { System.out.println(exc); }
  }

}
 

Następująca aplikacja:

import java.io.*;
import java.awt.*;
import java.util.*;
 

public class Serial {

   public static void main(String[] args)
                 throws IOException, ClassNotFoundException {
    Slist sl = null;
    if (args.length == 0) {
        FileInputStream in = new FileInputStream("SLIST.SER");
        ObjectInputStream s = new ObjectInputStream(in);
        sl = (Slist) s.readObject();
       }
     else {
        FileInputStream in = new FileInputStream("KOLORY.SER");
        ObjectInputStream ois = new ObjectInputStream(in);
        Hashtable ht = (Hashtable) ois.readObject();
        Color c = (Color) ht.get(args[0]);
        sl = new Slist();
        sl.getList().setBackground(c);
        for (int i = 0; i < args.length - 1; i++) sl.getList().add(args[i+1]);
        }

    if (sl != null) {
       sl.pack();
       sl.setVisible(true);
       }
   }

}
 


INNE KLASY WE-WY