5. Wejście-wyjście


 
Programowanie operacji wejścia-wyjścia w Javie nie jest banalne, choćby ze względu na bardzo dużą liczbę klas, które temu służą. Tu poznamy prawie wszystkie te klasy, ale przede wszystkim koncepcje, leżące u podstaw konstrukcji niezwykle bogatego w możliwości, ale jednocześnie dość zawikłanego środowiska  programowania wejścia-wyjścia w Javie.
Materiał jest obszerny i należy go przerabiać jako dwa kolejne wykłady, traktując przy tym fragmenty o "nowym wejściu-wyjściu" w Javie jako - w szczegółach - fakultatywne.
 

5.1. O wejściu-wyjściu w Javie

Java dostarcza dwóch podstawowych pakietów (z podpakietami), służących do przeprowadzania operacji wejścia-wyjścia:
Pakiet java.io zawiera przede wszystkim klasy, które pozwalają operować na strumieniach danych.


Strumień danych jest pojęciem abstrakcyjnym, logicznym.

Oznacza ciąg danych, właśnie „strumień”, do którego dane mogą być dodawane i z którego dane mogą być pobierane.

Przy czym:
  • strumień związany jest ze źródłem lub odbiornikiem danych
  • źródło lub odbiornik mogą być dowolne: plik, pamięć, URL, gniazdo, potok ...
  • strumień służy do zapisywania-odczytywania informacji - dowolnych danych
  • program:
    • kojarzy strumień z zewnętrznym źródłem/odbiornikiem,
    • otwiera strumień,
    • dodaje lub pobiera dane ze strumienia,
    • zamyka strumień.


Klasy reprezentujące strumienie (inaczej: klasy strumieniowe) omówimy najpierw, są one bowiem podstawowym środkiem programowania operacji wejścia-wyjścia w Javie.

W pakiecie java.nio ("Java new input-output", w skrócie NIO) wprowadzono dodatkowe środki wejścia-wyjścia, takie jak kanały, bufory i selektory . Mimo nazwy ("new input-output") środki te nie zastępują klas strumieniowych. Służą przede wszystkim do zapewnienia wysokiej efektywności i elastyczności programów, które w bardzo dużym stopniu obciążone są operacjami wejścia-wyjścia. W szczególności dotyczy to serwerów, które muszą równolegle obsługiwać ogromną liczbę połączeń sieciowych. Elementom nowych klas wejściowo-wyjściowych przyjrzymy się na końcu wykładu, bardziej zaawansowaną dyskusję tego tematu odkładając jednak do następnego semestru, gdy poznamy zasady programowania klient-serwer.   

Oprócz tego Java dostarcza klas reprezentujących inne od strumieni obiekty operacji wejścia-wyjścia.
Do klas tych należy np. klasa File z pakietu java.io - opisująca pliki i katalogi, a także - w pakiecie java.net - klasy reprezentujące obiekty "sieciowe", takie jak URL czy gniazdko (socket), mogące stanowić źródło lub odbiornik danych w sieci (w szczególności w Internecie).
Obiekty tych klas nie stanowią strumieni. Do operowania na nich strumienie (lub kanały) są jednak potrzebne i możemy je uzyskać albo przez użycie odpowiednich konstruktorów lub metod.


5.2. Klasy strumieniowe

Na strumieniach możemy wykonywać dwie podstawowe operacje: odczytywanie danych i zapisywanie danych. Z tego punktu widzenia możemy mówić o strumieniach wejściowych i wyjściowych. I odpowiednio do tego – Java wprowadza dwie rozłączne hierarchie klas strumieniowych: klasy strumieni wejściowych i klasy strumieni wyjściowych.

Dalej, pobranie/zapis danych może dotyczyć określonych  atomistycznych (minimalnie rozróżnialnych w trakcie operacji) „porcji danych”. Okazuje się, że nie są to tylko bajty. W Javie – ze względu na przyjęcie standardu kodowania znaków (Unikod) – wyróżnia się również inny „atom danych” – znak Unikodu, złożony z dwóch bajtów. Wobec tego powstają kolejne dwie rozłączne hierarchie klas strumieniowych: klasy strumieni  bajtowych („atomem” operacji we-wy jest bajt) oraz klasy strumieni znakowych („atomem” są znaki Unikodu – 2 bajty).

Przy przetwarzaniu tekstów należy korzystać ze strumieni znakowych ze względu na to, iż w trakcie czytania/pisania wykonywane są odpowiednie operacje kodowania/dekodowania ze względu na stronę kodową właściwą dla źródła/odbiornika


Zatem mamy aż cztery hierarchie klas strumieniowych.

Początkowe nadklasy tych hierarchii pokazuje poniższa tabela.


WejścieWyjście
Strumienie bajtoweInputStreamOutputStream
Strumienie znakoweReaderWriter

Są to klasy abstrakcyjne, zatem bezpośrednio nie można tworzyć obiektów tych klas.
Dostarczają one  natomiast podstaw dla wszystkich innych klas strumieniowych oraz paru ogólnych użytecznych (choć  bardzo podstawowych)  metod. Metody te umożliwiają m.in.  
Metody te są zazwyczaj odpowiednio przedefiniowane w klasach dziedziczących; polimorfizm zapewnia ich oszczędne i właściwe użycie.

Dzięki temu możemy np. opracować ogólną klasę udostępniającą rudymentarne kopiowanie strumieni.

import java.io.*;

class Stream {

  static void copy(InputStream in, OutputStream  out) throws IOException {
    int c;
    while ((c = in.read()) != -1) out.write(c);
  }

  static void copy(Reader in, Writer out) throws IOException {
    int c;
    while ((c = in.read()) != -1) out.write(c);
  }
}

Uwaga: metoda read() zwraca liczbę całkowitą, reprezentującą kolejny znak ze strumienia znakowego (lub bajt ze strumienia bajtowego) albo wartość -1 gdy czytanie sięga poza koniec pliku.

Możemy teraz użyć  metody copy wobec dowolnych strumieni z odpowiednich konkretnych klas hierarchii klas strumieniowych, np.

    Stream.copy(input, output);

Właśnie! Po to by kopiowanie miało sens input musi oznaczać konkretne źródło danych , a output – konkretny odbiornik danych.

Strumień abstrakcyjny (w którymś momencie) musi być związany z konkretnym źródlem bądź odbiornikiem.

W Javie jest to możliwe głównie (ale nie tylko) dzięki wprowadzeniu na kolejnych szczeblach dziedziczenia  omawianych czterech hierarchii (we-wy, bajty-znaki) konkretnych klas oznaczających różne rodzaje źródła/odbiornika danych. Można by je nazwać klasami przedmiotowymi, bowiem mają one  ustalone „przedmioty” operacji - konkretne rodzaje źródła bądź odbiornika.

5.3. Strumieniowe klasy przedmiotowe. Wiązanie strumieni ze źródłem/odbiornikiem

Źródła bądź odbiorniki danych mogą być różnorodne. Strumień może być związany np. z plikiem, z pamięcią operacyjną,  z potokiem, z URLem, z gniazdkiem (socket)....
Klasy przedmiotowe wprowadzono dla wygody operowania na konkretnych rodzajach żródeł i odbiorników.

Klasy przedmiotowe


Źródło/odbiornikStrumienie znakoweStrumienie bajtowe
PamięćCharArrayReader,
CharArrayWriter
ByteArrayInputStream,
ByteArrayOutputStream
StringReader,
StringWriter
StringBufferInputStream
PotokPipedReader,
PipedWriter
PipedInputStream,
PipedOutputStream
PlikFileReader,
FileWriter
FileInputStream,
FileOutputStream

Teraz już możemy użyć przykładowej (pokazanej poprzednio) klasy Stream  np.do kopiowania plików tekstowych i do zapisu zawartości łańcucha znakowego do pliku

class StreamCopy1 {
  static public void main(String[] args)  {
    try {
      FileReader in1 = new FileReader("plik0.txt");
      FileWriter out1 = new FileWriter("plik1.txt");
      Stream.copy(in1, out1);
      in1.close();
      out1.close();

      String msg = "Ala ma kota";
      StringReader in2 = new StringReader(msg);
      FileWriter out2 = new FileWriter("plik2.txt");
      Stream.copy(in2, out2);
      in2.close();
      out2.close();
    } catch(IOException exc) {
      exc.printStackTrace();
    }

  }
}
Komentarze:
Użycie klas przedmiotowych nie jest jedynym sposobem związania logicznego strumienia z fizycznym źródłem lub odbiornikiem.
Inne klasy (spoza pakietu java.io, np. klasy sieciowe) mogą dostarczać metod, które zwracają jako wynik referencję do abstrakcyjnego strumienia związanego z konkretnym źródłem odbiornikiem (np. plikiem w Sieci).

5.4. Klasy przetwarzające (przekształacanie danych w trakcie operacji na strumieniach)

Przy wykonywaniu operacji we-wy mogą być dokonywane przekształcenia danych.
Java oferuje nam wiele klas wyspecjalizowanych w konkretnych rodzajach automatycznego przetwarzania strumieni. Klasy te implementują określone rodzaje przetwarzania strumieni, niezależnie od rodzaju źródła/odbiornika

Rodzaj przetwarzania Strumienie znakoweStrumienie bajtowe
BuforowanieBufferedReader,
BufferedWriter
BufferedInputStream,
BufferedOutputStream
FiltrowanieFilterReader,
FilterWriter
FilterInputStream,
FilterOutputStream
Konwersja: bajty-znakiInputStreamReader,
OutputStreamWriter
 
Konkatenacja SequenceInputStream
Serializacja obiektów ObjectInputStream,
ObjectOutputStream
Konwersje danych DataInputStream,
DataOutputStream
Zliczanie wierszyLineNumberReaderLineNumberInputStream
PodglądaniePushbackReaderPushbackInputStream
DrukowaniePrintWriterPrintStream

Komentarze:

Można tworzyć własne filtry.
Konstruktory klas przetwarzających mają jako argument referencję do obiektów podstawowych klas abstrakcyjnych hierarchii dziedziczenia (InputSteram, OutputSteram, Reader, Writer).
Dlatego przetwarzanie (automatyczna transformacja) danych jest logicznie oderwana od fizycznego strumienia, stanowi swoistą na niego nakładkę.

Zatem zastosowanie klas przetwarzających wymaga:
Przykłady tego zobaczymy za chwilę. Przedtem jednak - dla lepszej orientacji w gąszczu klas strumieniowych - warto przedstawić ich hierarchie dziedziczenia.

5.5. Hierarchie dziedziczenia klas strumieniowych


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 przetwarzające (realizujące określone rodzaje orzetwarzania).

Klasy dla strumieni bajtowych

r

r

Żródło: Java Tutorial, Sun Microsystems 2002
 

Klasy dla strumieni znakowych

r

r

Żródło: Java Tutorial, Sun Microsystems 2002


5.6. Buforowanie

Buforowanie ogranicza liczbę fizycznych odwołań do urządzeń zewnętrznych, dzięki temu, że fizyczny odczyt lub zapis dotyczy całych porcji danych, gromadzonych w buforze (wydzielonym obszarze pamięci). Jedno fizyczne odwołanie wczytuje dane ze strumienia do bufora lub zapisuje zawartość bufora do strumienia. W naszym programie operacje czytania lub pisania dotyczą w większości bufora (dopóki są w nim dane lub dopóki jest miejsce na dane) i tylko niekiedy  powodują fizyczny odczyt (gdy bufor jest pusty) lub zapis (gdy bufor jest pełny).

Np. przy czytaniu dużych plików tekstowych należy unikać bezpośredniego czytania za pomocą klasy FileReader. To samo dotyczy zapisu.
Zastosowanie klasy BufferedReader (czy BufferedWriter) powinno przynieść poprawę efektywności działania programu.
Ale klasa BufferedReader (BufferedWriter) jest klasą przetwarzającą, a wobec tego w jej konstruktorze nie możemy bezpośrednio podać fizycznego źródła danych.

Np. przy czytaniu plików źródło to podajemy przy konstrukcji obiektu typu FileReader, a po to, żeby uzyskać buforowanie, "opakowujemy" FileReadera BufferedReaderem.

Wygląda to mniej więcej 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
// czytamy wiersz po wierszu
String line;
while  ((line = br.readLine()) != null) {      // kolejny wiersz pliku:
                                               // metoda readLine zwraca wiersz
                                               // lub null jeśli koniec pliku
   // ... tu coś robimy z odczytanym wierszem
}
Uwaga.
W konstruktorach klas Buffered... możemy podać rozmiar bufora. Domyślny rozmiar jest wystarczający dla codziennych zastosowań.

Przykład buforowania: program, czytający plik tekstowy i zapisujący jego zwartośc do innego pliku wraz z numerami wierszy.

import java.io.*;

class Lines {
	
  public static void main(String args[]) {
    try {
      FileReader fr = new FileReader(args[0]);
      LineNumberReader lr = new LineNumberReader(fr);
      BufferedWriter bw = new BufferedWriter(
                              new FileWriter(args[1]));

      String line;
      while  ((line = lr.readLine()) != null) {
        bw.write( lr.getLineNumber() + " " + line);
        bw.newLine();
      }
      lr.close();
      bw.close();
    } catch(IOException exc) {
        System.out.println(exc.toString());
        System.exit(1);
   }

  }
}
Komentarze:
Analogicznie postępujemy przy buforowaniu plików wyjściowych.

5.7. Strumienie binarne

Klasy przetwarzające DataInputStream i  DataOutputStream służą do odczytu/zapisu danych typów pierwotnych w postaci binarnej (oraz łańcuchów znakowych).

Metody tych klas mają postać:

typ readTyp( )  
void writeTyp(typ arg)

gdzie typ odpowiada nazwie któregoś z typów pierwotnych
Mamy więc np. metody readInt(),  readDouble() itp.

Dane typu String mogą być zapisywane/czytane do/z strumieni binarnych za pomocą metod writeUTF i readUTF.

Dla przykładu, stwórzmy klasę Obs, której obiekty reprezentują obserwacje. Każda obserwacaja ma nazwę oraz odpowiadający jej ciąg (tablicę) liczb rzeczywistych. Może to być np. MAX_TEMPERATURA z 12 liczbami, pokazującymi maksymalną temperaturę w 12 miesiącach roku.
W klasie tej zdefiniujemy także dwie metody, slużące do zapisu obserwacji w postaci binarnej do strumienia i odczytywania binarnych strumieni obserwacji.

import java.io.*;

class Obs {
  String name;
  double[] data;

  public Obs() {}

  public Obs(String nam, double[] dat) {
    name = nam;
    data = dat;
  }

  public void writeTo(DataOutputStream dout)
         throws IOException  {
    dout.writeUTF(name);
    dout.writeInt(data.length);
    for (int i=0; i<data.length; i++) dout.writeDouble(data[i]);
  }

  public Obs readFrom(DataInputStream din)
         throws IOException {
    name = din.readUTF();
    int n = din.readInt();
    data = new double[n];
    for (int i=0; i<n; i++) data[i] = din.readDouble();
    return this;
  }

  public void show() {
    System.out.println(name);
    for (int i=0; i<data.length; i++) System.out.print(data[i] + " ");
    System.out.println("");
  }
}
Zwróćmy uwagę, że przyjęliśmy następujący format zapisu obserwacji w pliku binarnym:
nazwa
liczba_elementów_tablicy
dane_tablicy.

Dzięki temu, metoda readFrom bez kłopotu może odczytywać dowolne obserwacje z dowolnych plików binarnych, pod warunkiem, że pliki te mają podany format.
 

Przykład wykorzystania klasy: tworzymy dwie obserwacje, pokazujemy jak wyglądają (show) zapisujemy je do pliku (writeTo(out)), po czym z tego samego pliku odczytujemy dane do innych (ad hoc tworzonych) obiektów-obserwacji i jednocześnie pokazujemy odczytane dane na konsoli (new Obs().readFrom(in).show()).

class BinDat {
	
  public static void main(String args[]) {
    double[] a = { 1, 2, 3, 4 };
    double[] b = { 7, 8, 9, 10 };

    Obs obsA = new Obs("Dane A", a);
    Obs obsB = new Obs("Dane B", b);

    obsA.show();
    obsB.show();

    try {
      DataOutputStream out = new DataOutputStream(
                                 new FileOutputStream("dane")
                                 );
      obsA.writeTo(out);
      obsB.writeTo(out);
      out.close();

      DataInputStream in = new DataInputStream(
                                 new FileInputStream("dane")
                                 );
      new Obs().readFrom(in).show();
      new Obs().readFrom(in).show();
      in.close();
      } catch (IOException exc) {
        exc.printStackTrace();
        System.exit(1);
        }


  }
}



5.8. Kodowanie

Java posługuje się znakami w formacie Unicode. 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 znaki Unikodu 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 strony kodowej - przy konwersjach zostanie przyjęta domyślna strona kodowa.
Aby się dowiedzieć, jakie jest domyślne kodowanie 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 stron kodowych, które będą używane do kodownia i dekodowania bajty-znaki .
Napiszmy program wykonujący konwersje plików z-do dowolnych (dopuszczalnych przez Javę) formatów kodowania.
Dopuszczalne symbole kodowania można znaleźć na stronach java.sun.com.

import java.io.*;

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();
       }
       in.close();
       out.close();
    } catch (IOException e) {
        System.err.println(e);
        System.exit(1);
    }

  }
}

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

O innych sposobach kodowania i dekodowania danych wejściowo-wyjściowych - zob. punkt dotyczący "nowego wejścia-wyjścia" (java.nio).

5.9. Serializacja obiektów

Obiekty  tworzone przez program  rezydują w pamięci operacyjnej, w przestrzeni adresowej procesu.
Są zatem nietrwałe, bo kiedy program kończy działanie wszystko co znajduje się w jego przestrzeni adresowej ulega wyczyszczeniu i nie może być odtworzone.

Serializacja (szeregowanie) pozwala na utrwalaniu obiektów.
W Javie polega ona na zapisywaniu obiektów do strumienia.


Podstawowe zastosowania serializacji:
  • komunikacja pomiędzy obiektami/aplikacjami poprzez gniazdka (sockets),
  • zachowanie obiektu (jego stanu i właściwości) do późniejszego odtworzenia i wykorzystania przez tę samą lub inną aplikację.


Do zapisywania/odczytywania obiektów służą klasy ObjectOutputStream oraz ObjectInputStream, które należą do strumieniowych klas przetwarzających.


Metoda klasy ObjectInputStream:

            void writeObject(Object o)  zapisuje obiekt o do strumienia

Metoda klasy ObjectInputStream:

            Object readObjectO   odczytuje obiekt ze strumienia i zwraca referencję do niego
   

Do strumieni mogą być zapisywane tylko serializowalne obiekty.
Obiekt jest serializowalny jeśli jego klasa implementuje interfejs Serializable

Prawie wszystkie klasy standardowych pakietów Javy implementują ten interfejs.
Również tablice (które są obiektami specjalnych klas definiowanych w trakcie kompilacji) są serializowalne.

Zatem bez problemu możemy utrwalać obiekty większości klas standardowych Javy oraz tablice.
Przykład: program zapisuje do strumienia obiekty - datę, tablicę opisów i odpowiadającą każdemu opisowi temperaturę. Następnie odczytuje te obiekty ze strumienia i odtwarza je.

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

class Serial {
	
  public static void main(String args[]) {

    Date data = new Date();
    int[] temperatura = { 25, 19 , 22};
    String[] opis = { "dzień", "noc", "woda" };

    // Zapis
    try {

      ObjectOutputStream out = new ObjectOutputStream(
                                 new FileOutputStream("test.ser")
                                 );
      out.writeObject(data);
      out.writeObject(opis);
      out.writeObject(temperatura);
      out.close();
    } catch(IOException exc) {
        exc.printStackTrace();
        System.exit(1);
    }

    // Odtworzenie (zazwyczaj w innym programie)
    try {
      ObjectInputStream in = new ObjectInputStream(
                                 new FileInputStream("test.ser")
                                );
      Date odczytData = (Date) in.readObject();
      String[] odczytOpis = (String[]) in.readObject();
      int[] odczytTemp = (int[]) in.readObject();
      in.close();
      System.out.println(String.valueOf(odczytData));
      for (int i=0; i<odczytOpis.length; i++)
          System.out.println(odczytOpis[i] + " " + odczytTemp[i]);

    } catch(IOException exc) {
        exc.printStackTrace();
        System.exit(1);
    } catch(ClassNotFoundException exc) {
        System.out.println("Nie można odnaleźć klasy obiektu");
        System.exit(1);
    }

  }

}
Przykładowy wydruk programu.

Wed Jan 15 18:30:17 CET 2003
dzień 25
noc 19
woda 22


Metoda readObject() pobiera ze strumienia zapisane charakterystyki obiektu (w tym również oznaczenie klasy do której należy zapisany obiekt) - na ich podstawie tworzy nowy obiekt tej klasy i inicjuje go odczytanymi wartościami. Wynikiem jest referencja formalnego typu Object wskazująca na nowoutworzony obiekt, który jest identyczny z zapisanym. 
Ponieważ wynikiem jest Object, nalezy  wykonać odpowiednią konwersję zawężającą do właściwego typu (referencji do konkretnej podklasy klasy Object, tej mianowicie, której egzemplarzem faktycznie jest odczytany obiekt).
Może się też okazać, że w strumieniu zapisano obiekt klasy, która nie jest dostępna przy odczytywaniu (np. została usunięta). Wtedy przy tworzeniu obiektu z odczytanych danych powstanie wyjątek ClassNotFoundException, który musimy obslugiwać.

A jak serializować obiekty własnych klas?
Odpowiedź już znamy: klasa winna implementować interfejs Serializable.

Takie interfejsy (bez metod) nazywane są interfejsami znacznikowymi. Ich jedyną funkcją jest umożliwienie sprawdzenia typu np. za pomocą operatora instanceof. Metoda writeObject to własnie robi, gdy podejmuje decyzje o zapisie: jeśli jej argument x jest typu Serializable (x instanceof Serializable ma wartośc true), to obiekt jest zapisywany do strumienia, w przeciwnym razie - nie
Rzecz nie jest trudna, bowiem interfejs ten jest pusty - nie musimy więc implementować żadnych jego metod, wystarczy tylko wskazać, że nasza klasa implementuje interfesj Serializable.

Zobaczmy to na przykladzie nieco bardziej praktycznego zastosowania serializacji.
Przypomnijmy sobie klasę TravelSearcher z poprzedniego semestru. Slużyła ona do zgromadzenia w tablicy obiektów klasy Travel, opisujących destynacje (napis, np. Cypr) i ceny podróży (liczba całkowita, np. 1500) oraz dostarczała metody wyszukiwania informacji o cenie na podstawie podanej destynacji.
Klasa Travel jest bardzo prosta:
public class Travel ... {

   private String dest; // destynacja podrózy
   private int price;   // cena

   public Travel(String s, int p) {
     dest = s;
     price = p;
   }

   public String getDest() { return dest; }
   public int getPrice() { return price; }
   public String toString() { return dest + ", cena: " + price; }

}

Schemat klasy TravelSearcher przedstawiono poniżej:
public class TravelSearcher ... {

  private Travel[] travel;            // tablica podróży
  private int lastIndex = -1;         // indeks ostatnio zapisanej
  private final int MAX_COUNT = 5;    // max rozmiar tablicy
  private boolean sorted = false;     // czy jest posortowana

  // Konstruktor: tworzy tablicę
  public TravelSearcher() {
    travel = new Travel[MAX_COUNT];
  }

  // Metoda add dodaje nowy element do tablicy
  // jeżeli przekrozcono zakres
  // - zgłaszany jest wyjątek własnej klasy NoSpaceForTravelException
  public void add(Travel t) throws NoSpaceForTravelException {
    try {
      lastIndex++;
      travel[lastIndex] = t;
    } catch (ArrayIndexOutOfBoundsException exc) {
        lastIndex--;
        throw new NoSpaceForTravelException("Brakuje miejsca dla dodania podróży");
    }
    sorted = false;
  }

  // Jaki jest ostatni zapisany indeks
  public int getLastIndex() { return lastIndex; }


  // Wyszukiwanie podróży na podstawie podanego celu (destynacji)
  public Travel search(String dest) {
    if (!sorted) sortByDest();
    // ... wyszukiwanie binarne
  }

  // Sortowanie - aby można było stosować wyszukiwanie binarne
  private void sortByDest() {
    // ... sortowanie
    sorted = true;
  }

  public String toString() {
    // zwraca spis podróży z tablicy travel (destynacji i cen)
  }

}

Wyobraźmy sobie, że w innej klasie dostarczamy jakiś interfejs użytkownika, umożliwiający wprowadzanie informacji o podróżach i na tej podstawie tworzenie obiektów klasy Travel oraz wpisywanie ich do tablicy w klasie TravelSearcher.
Informacje podawane są na bieżąco (w jakichś oknach dialogowych) i zapisywane j dodawane do tablicy travel w klasie TravelSearcher.
W trakcie dzialania tego programu możemy zapewnić wyszuskiwanie informacji o wpisanych podróżach. Ale gdy program zakończy działanie - obiekt klasy TravelSearcher, a co za tym idzie cala tablica informacji o podróżach - znikną.
Możemy temu zaradzić zapewniając zapis informacji do pliku. Jeśli zastosujemy zwykłe strumienie (np. znakowe), to - oczywiście - w dość prosty sposób możemy zapisać do pliku listę podróży i wprowadzić ją znowu przy ponownym uruchomieniu aplikacji.

Dużo prościej jednak będzie zapisać  do strumienia obiekt klasy TravelSearcher.
Dodatkowo zyskamy całkowite odtworzenie stanu aplikacji (obiektu TravelSearcher) zapamiętanego przy poprzednim jej uruchomieniu. A ten stan, to nie tylko tablica wycieczek, ale również istotne dla dzialania aplikacji wartości indeksu ostatniego zapisanego elementu tablicy oraz zmiennej sorted pokazującej czy tablica jest posortowana.

Zatem należy zagwarantować serializowalność obiektów klasy TravelSearcher oraz - w głównej aplikacji (określającej interfejs użytkownika) zapewnić ich serializację poprzez użycie metody writeObject oraz odtwarzanie - za pomoca metody readObject.

Napiszemy więc na pewno:

public class TravelSearcher implements Serializable {
        ...
}


Czy to wystarczy? Zwróćmy uwagę, że polem klasy TravelSearcher jest tablica referencji do obiektów klasy Travel. Tablice - jak widzieliśmy poprzednio - zapisują się do strumieni obiektowych bez kłopotu. A co z obiektami klasy Travel, na które wskazują elementy tablicy?

Szczęśliwie:

Przy serializacji zapisywany i odtwarzany jest pełny stan obiektu (w tym - rekursywnie - obiektów składowych).

Ale nie są  zapisywane stany obiektów składowych, które należą do klas nieserializowalnych. Bo choć pola, odpowiadające tym obiektom są zapisywane, to przy odtwarzaniu, takie obiekty są tworzone za pomocą konstruktorów bezparametrowych z ich klas i nie ma żadnej innej inicjacji ich elementów.

Musimy zatem zapewnić również serializację obiektów klasy Travel:

public class Travel implements Serializable {
        ...
}


Po tych poprawkach, klas TravelSearcher i Travel możemy użyć w przykladowej aplikacji, stanowiącej interfejs użytkownika do wprowadzania, wyszukiwania, zapisywania i lstowania podróży.

Argumentem aplikacji jest plik "kartoteka", który zawiera lub będzie zawierał utrwalony obiekt klasy TravelSearcher. Jeżeli taki plik już istnieje, to informacje o wycieczkach są z niego odtwarzana za pomoca deserializacji utrwalonego obiektu. Użytkownik ma do wyboru różne tryby działania (np. wprowadzanie nowych danych, ich wyszukiwanie i - oczywiście - utrwalenie.

Pokazuje to poniższy program.
Przy okazji warto zwrócić uwagę na sposoby oprogramowania dialogow wyboru (zastosowane tu postaci metod z klasy JOptionPane będą wyjaśnione w następnych wykładach; już teraz można jednak samodzielnie zapoznać się z nimi na podstawie dokumentacji).

import java.io.*;
import java.util.*;
import javax.swing.*;

public class TravelApp {

  private String travFileName;
  private TravelSearcher travels;
  private boolean dataSaved = false;

  public TravelApp(String[] tfn) {
    try {
      travFileName = tfn[0];
      ObjectInputStream in = new ObjectInputStream(
                                 new FileInputStream(travFileName)
                             );
      travels = (TravelSearcher) in.readObject();
      in.close();

    } catch(ArrayIndexOutOfBoundsException exc) {
        showMsg("Syntax: java TravelApp plik_kartoteki");
        System.exit(1);

    } catch(FileNotFoundException exc) {
        showMsg("Nowa kartoteka!!!");
        travels = new TravelSearcher();

    } catch(IOException exc) {
        exc.printStackTrace();
        System.exit(2);

    } catch(ClassNotFoundException exc) {
       showMsg("Brak klasy dostępu do klasy TravelSearcher");
       System.exit(3);
    }

    String[] modes = { "Wprowadzanie", "Szukanie", "Zapis", "Pokaz", "Koniec" };
    while (true) {
      switch ( select("Wybierz tryb działania", modes)) {
        case 'W' : inputData(); break;
        case 'S' : searchData(); break;
        case 'Z' : saveData(); break;
        case 'P' : showData(); break;
        case 'K' : finish(); break;
        default : break;
      }
    }
  }

  private char select(String msg, String[] modes) {
    int sel = JOptionPane.showOptionDialog(null, msg,
                         "Travel App", 0, JOptionPane.QUESTION_MESSAGE,
                          null, modes, modes[1]);
    if (sel == JOptionPane.CLOSED_OPTION) return 0;
    return modes[sel].charAt(0);
  }

  public void inputData() {
    String data = "";
    String msg = "Wprowadź dane";
    while((data = ask(msg, data)) != null) {
      StringTokenizer st = new StringTokenizer(data);
      try {
        String dest = st.nextToken();
        int price = Integer.parseInt(st.nextToken());
        travels.add(new Travel(dest, price));
        dataSaved = false;
      } catch(NoSpaceForTravelException exc) {
          showMsg(exc.getMessage());
          return;
      } catch(Exception exc) {
          msg = "Dane wadliwe - popraw";
          continue;
      }

      msg = "Wprowadź dane";
      data = "";
    }
  }

  public void searchData() {
    if (travels.getLastIndex() >= 0) {
      String dest = "";
      String msg = "Podaj miejsce podróży";
      while((dest = ask(msg, "")) != null) {
        Travel t = travels.search(dest);
        String info = (t == null ? "Nie ma takiej podróży!" : t.toString() );
        showMsg(info);
      }
    }
    else showMsg("Nie ma żadnych danych do przeszukiwania!");
  }

  public void saveData() {
    ObjectOutputStream out = null;
    try {
      out = new ObjectOutputStream(
                new FileOutputStream(travFileName)
                );
      out.writeObject(travels);
    } catch(IOException exc) {
        showMsg(exc.getMessage());
    } finally {
        try { out.close(); } catch (Exception exc) {}
    }
    dataSaved = true;
  }

  public void showData() {
    System.out.println("Dane\n" + travels);
  }

  public void finish() {
    while (!dataSaved) {
      char ans = select("Czy zapisać dane?", new String[] { "Tak", "Nie" } );
      if (ans == 'T')  saveData();
      else if (ans == 'N') break;
    }
    System.exit(0);
  }

  private void showMsg(String msg) {
    JOptionPane.showMessageDialog(null, msg);
  }

  private String ask(String msg, String initVal) {
    return JOptionPane.showInputDialog(null, msg, initVal);
  }

  public static void main(String[] args) {
    new TravelApp(args);
  }

}


Na koniec warto powiedzieć, że:

 private void readObject(java.io.ObjectInputStream stream)
     throws IOException, ClassNotFoundException;
 private void writeObject(java.io.ObjectOutputStream stream)
     throws IOException
 

5.10. Potoki

Potoki służą do przesyłania danych pomiędzy równolegle działającymi wątkami.


Wątek produkujący dane zapisuje je do potoku wyjściowego (PipedWriter lub PipedOutputStream).
Potok ten za pomocą konstruktorów potokowych klas wejściowych (PipedReader i PipedInputStream) można przyłączyć do strumienia wejściowego, z którego inny watek będzie czytał dane.

Niech na przyklad obiekt-wątek klasy DataPutter produkuje jakieś dane i umieszcza je w strumieniu wyjściowym, do którego referencję otrzymuje konstruktor.

class DataPutter extends Thread {

  OutputStream out;

  public DataPutter(OutputStream o) {
    out = o;
  }

  public void run() {
    try {
      for (char c = 'a'; c <= 'z'; c++) out.write(c);
      out.close();
    } catch(IOException exc) { return; }

  }
}

a obiekt-wątek klasy DataGetter, odczytuje jakieś dane ze strumienia i wypisuje je na konsoli.

class DataGetter extends Thread {

  InputStream in;

  public DataGetter(InputStream i) {
    in = i;
  }

  public void run() {
    try {
      int c;
      while ((c = in.read()) != -1) System.out.println((char) c);
    } catch(IOException exc) { return; }
  }
}

Za pomocą potoku możemy połączyć wyjściowy strumień, do którego pisze DataPutter z wejściowym strumieniem czytanym przez DataGetter.

W tym celu zwiążemy strumień wyjściowy, do którego ma pisać DataPutter z potokiem:
PipedOutputStream pout = new PipedOutputStream();
i potok ten połączymy ze strumieniem wejściowym, z którego będzie czytał DataGetter. 

    PipedInputStream pin = new PipedInputStream(pout);

Po uruchomieniu obu watków:

class Main {
  public static void main(String[] args) throws IOException {
    PipedOutputStream pout = new PipedOutputStream();
    PipedInputStream pin = new PipedInputStream(pout);
    new DataPutter(pout).start();
    new DataGetter(pin).start();
  }
}
uzyskamy oczekiwany wynik: produkowanie przez jeden z nich danych i przesylanie ich potokiem do drugiego wątku.
Uwaga: przy pisaniu/czytaniu znaków Unikodu należy stosować klasy PipeWriter i  PipeReader.

Podstawową zaletą potoków jest to, iż umożliwiają one uproszczenie komunikacji pomiędzy wątkami. Wątek zapisuje dane do potoku i o nic więej nie musi dbać. Inny wątek czyta dane z potoku za pomocą zwyklego read(), na którym - ew. jest blokowany, jeśli danych jeszcze nie ma. Nie musimy martwić się o synchronizację i koordynację dzialania wątków. Samo pisanie i czytanie danych za pomocą potoków taką synchronizację i koordynacje już zapewnia. 

Przykład-zadanie:
Wątek-Autor pisze teksty, skladając losowo wybrane słowa w wiersze o losowo wybranej liczbie słów. Wiersze te pobiera wątek-Duplikator i rozdysponowuje je do wielu wątków-przepisywaczy (TxtWriter), Każdy z przepisywaczy równolegle  wypisuje tekst Autora w przydzielonynym mu miejscu (nazwiemy je SpaceToWrite). 

Przed lekturą dalszego tekstu proszę spróbowac rozwiąza to zdanie samodzielnie.

Poniżej pokazano możliwe rozwiązanie. Klasy są bardzo szczegółowo komentowane, zatem dodatkowy opis programu ograniczymy do minimum.

Klasa Author zzapewnie generowanie tekstów autora. Są one tworzone w wątku głównym programu, zatem nie dostarczyliśmy tu metody run().

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

public class Author {

  private int linesToWrite;    // ile wierszy ma napisac autor
  String[] words;              // z jakich slów się będą składać
  private Writer out;          // strumień do którego zapisuje teksty
  static final int N = 5;      // maksymalna liczba słów w wierszu

  public Author(int l, String[] words, Writer w) {
    linesToWrite = l;
    this.words = words;
    out = w;
    try {
      write();                  // wywołanie pisania
    } catch(IOException exc) {
        System.out.println(exc.toString());
    } catch(InterruptedException exc) {}
  }

  // Metoda pisania przez autora
  public void write() throws IOException,
                             InterruptedException {
    Random rand = new Random();
    for (int i=0; i < linesToWrite; i++) {

      // Każdy wiersz składa się z losowo wybranej nw liczby słów
      int nw = rand.nextInt(N) + 1;
      String line = "";

      for (int k=0; k<nw; k++) {   // słowa są losowane z tablicy words
        int wordNum = rand.nextInt(words.length);
        line += words[wordNum] + " ";
      }
      out.write(line);
      out.write('\n');
      Thread.sleep((rand.nextInt(3) + 1) * 1000);  // autor myśi nad
    }                                              // następnym wierszem :-)
    out.write("Koniec pracy\n");
    out.close();
    System.out.println("Autor skończył pisać");
  }
} 
Każdy z przepisywaczy (klasa TxtWriter) stanowi odrębny wątek, czyta teksty ze strumienia  wypisuje je w miejscu określonym przez przekazany konstruktorowi obiekt SpaceToWrite. Generalnie może to być cokolwiek, na czym będzie widać tekst. Jak zobaczymy dalej - w tym programie zastosujemy wielowierszowe pola edycyjne umieszczone w jednym oknie (po jednym polu dla każdego z przepisywaczy).

import java.io.*;

public class TxtWriter extends Thread {  // Klasa przepisywacza

  private LineNumberReader in;   // strumień skąd czyta
  private SpaceToWrite spw;      // miejsce gdzie pisze

  public TxtWriter(String name,      // nazwa przepisywacza
                   Reader in_,       // z jakiego strumeinia czyta
                   SpaceToWrite spw_ // gdzie pisze
                   )
  {
    super(name);
    in = new LineNumberReader(in_);  // filtrowanie strumienia
                                     // by mieć numery wierszy
    spw = spw_;
  }

  // Kod wątku przepisywacza
  // czyta wiersze ze strumienia wejściowego
  // i zapisuje je w miejscu oznaczanym spw (SpaceToWrite)
  // dopóki nie nadszedl sygnał o końcu pracy (tekst "Koniec pracy")
  public void run() {
    spw.writeLine(" *** " + getName() + " rozpoczął pracę" + " ***");
    spw.writeLine("---> czekam na teksty !");
    String txt;
    try {
      txt = in.readLine();
      while(!txt.equals("Koniec pracy")) {
        spw.writeLine(in.getLineNumber() + " " + txt);
        txt = in.readLine();
      }
      in.close();
      spw.writeLine("**** " + getName() + " skończył pracę");
    } catch(IOException exc) {
        spw.writeLine("****" + getName() + " - zakonczenie na skutek bledu");
        exc.printStackTrace();
        return;
    }
  }
}

Wątki przepisywaczy tworzy i uruchamia Duplikator. Pośredniczy on rownież w przekazywaniu tekstów od autora do przepisywaczy i czyni to właśnie za pomocą potoków.

import java.io.*;

public class Duplicator extends Thread {

  PipedReader fromAuthor;    // potok od autora
  PipedWriter[] toWriters;   // potoki do przepisywaczy

  public Duplicator(PipedReader pr,       // potok od autora
                    SpaceToWrite[] space  // na czym piszą pzrepisywacze?
                    ) throws IOException {
    fromAuthor = pr;

    int numOfWriters = space.length;      // tylu jest przepisywaczy
                                          // ile miejsc na których piszą

    // Tworzymy tablicę potoków do przepisywaczy
    toWriters = new PipedWriter[numOfWriters];

    for (int i = 0; i < numOfWriters; i++) { // dla każdego przepisywacza

      // tworzymy potok do niego
      toWriters[i] = new PipedWriter();

      // tworzymy przepisywacza
      // podając: nazwę, z jakiego potoku ma czytać, miejsce gdzie ma pisać
      TxtWriter tw = new TxtWriter("TxtWriter " + (i+1),
                                   new PipedReader( toWriters[i]), // połączenie!
                                   space[i]);

      // uruchamiamy wątek przepisywacza
      tw.start();
    }
  }

  // Kod wykonywany w wątku Duplikatora
  public void run() {
    try {
      // Buforowanie potoku od autora
      BufferedReader in = new BufferedReader(fromAuthor);

      // czytanie wierszy z potoku od autora
      // i zapisywanie ich do potoków, czytanych przez przepisywaczy
      while (true) {
        String line = in.readLine();
        for (int i = 0; i < toWriters.length; i++) {
          toWriters[i].write(line);
          toWriters[i].write('\n');
        }
        if (line.equals("Koniec pracy")) break;
      }
    } catch (IOException exc) { return; }
    System.out.println("Duplikator zakończył działanie");
  }

}

Główna klasa aplikacji organizuje cały ten "proceder".

import java.io.*;

class PipesShow {

  PipedWriter authorWrites = new PipedWriter(); // potok, do którego pisze autor
  PipedReader duplicatorReads;                  // potok, z ktorego czyta duplikator

  Duplicator dup;

  PipesShow(int numLines, int numWriters) {

    // każdy przepisywacz na swoją przestrzeń pisania
    SpaceToWrite[] writeSpace = new SpaceToWrite[numWriters];
    for (int i=0; i < writeSpace.length; i++)
      writeSpace[i] = new SpaceToWrite(20, 30); // 20 wierszy, 30 kolumn

    try {
      // Połączenie potoku do ktorego pisze autor
      // z nowoutworzonym potokiem, z którego będzie czytał duplikator
      duplicatorReads = new PipedReader(authorWrites);

      // utworzenie duplikatora (on z kolei stworzy i uruchomi przepisywaczy)
      dup = new Duplicator(duplicatorReads, // skąd będzie czytał
                           writeSpace);     // przetstrzeń pisania dla przepisywaczy

      // start wątku duplikatora
      dup.start();

    } catch (IOException exc) {
        System.out.println("Nie można stworzyć duplikatora");
        exc.printStackTrace();
        System.exit(1);
    }

    SpaceToWrite.show(numWriters); // pokazanie ogólnej przestrzeni pisania
                                   // grupującej przestrzenie pisania
                                   // każdego przepisywacza

    // Teraz autor będzie pisał!
    // Utworzenie obiektu klasy Autor powoduje rozpoczęcie przez niego pisania

    String words[] = { "Ala", "ma", "kota", "i", "psa" };

    Author autor = new Author(numLines,      // ile wierszy ma napisać
                              words,         // z jakich słów składać teksty
                              authorWrites); // Dokąd je zapisywać
  }

  public static void main(String args[]) {
    int numLin = 0; // ile wierszy ma napisać autor
    int numWri = 0; // ilu jest przepisywaczy
    try {
      numLin = Integer.parseInt(args[0]);
      numWri = Integer.parseInt(args[1]);
    } catch(Exception exc) {
        System.out.println("Syntax: java  PipesShow numLines numWri");
        System.exit(1);
    }
    new PipesShow(numLin, numWri);
  }
}

No i w końcu przestrzeń przepisywania. Uprzedzając nieco wykłady o graficznych interfejscah użytkownika zastosujemy tu proste elementy AWT. Jeżeli w tej chwili będzie to niezrozumiałe - proszę się nie martwić (dzięki klasie SpaceToWrite odseparowaliśmy wygląd aplikacji od jej funkcjonalności, zatem z punktu widzenia eksperymentowania z potokami nie ma większego znaczenia jaką postać ma ta klasa).

// Klasa, określająca przestrzenie
// na których piszą przepisywacze
// oraz grupująca te przestrzenie w oknie.
// Każdy przepisywacz wypisuje tekst
// do wielowierszowego pola edycyjnego (TextArea z pakietu AWT)
// do czego służy mu metoda writeLine.
// Wszystkie przestrzenie grupowane są w oknie frame.

import java.awt.*;
import java.awt.event.*;

public class SpaceToWrite extends TextArea {

  private static Frame frame = new Frame("Write space");

  // Konstruktor: tworzy nową przetrzeń pisania dla jednego przepisywacza
  public SpaceToWrite(int rows, int cols) {
    super(rows, cols);  // utworzenie TextArea  - z podaną liczbą wierszy, kolumn
    frame.add(this);    // dodanie TextArea do okna
  }

  // Metoda dopisująca nowy wiersz do textarea
  public void writeLine(String s) {
    this.append(s + '\n');
  }

  // Metoda ustalająca ułożenie pól edycyjnych w oknie
  // rozmiar okna (pack daje rozmiar taki jak akurat potrzreba)
  // i pokazująca okno
  public static void show(int numWriters) {
    frame.setLayout(new GridLayout(0, numWriters));
    frame.pack();
    frame.show();

    // Umożliwienie zakończenia aplikacji poprzez zamknięcie okna
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        frame.dispose();
        System.exit(1);
      }
    });
  }

Wynik dzialania całego programu pokazuje poniższy rysunek:
r

Dla prześledzenia równoleglej pracy przepisywaczy proszę skompilować i uruchomić ten program na własnym komputerze (program znajduje się w katalogu PipesTest).


5.11. 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. Zazwyczaj, jeśli zasobem jest plik - uzyskamy możliwość odczytania tego pliku.

Istnieją dwa elementarne  sposoby działania na strumieniach z Internetu:  


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.*;

class URLReader {

 public static void main(String[] args) throws Exception {
   BufferedReader list = new BufferedReader(           // lista URLi
                             new FileReader(args[0]));
   String urlString;
   while ((urlString = list.readLine()) != null) {
      readAndSave(new URL(urlString)); // tworzony nowy obiekt klasy URL
      }                                // oznaczający zasób z Sieci
   list.close();
   System.exit(0);

 }


 static void readAndSave(URL url) throws Exception {

   BufferedReader in = new BufferedReader(
                         new InputStreamReader(
                          url.openStream()   // zwraca InputStream związany z URLem
                          )
                          );

   String fname = null;
   StringTokenizer st = new StringTokenizer(
                            url.getFile(),    // <- zwraca nazwę pliku dla URLa
                            "/"
                            );
   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();
   }

}

Inne sposoby dzialania w Sieci, szczególnie w ujęciu klient-serwer (m.in. komunikacja przez gniazda - sockets) zostaną omówione w przyszłym semestrze.

5.12. Obiekty plikowe

Klasa File oznacza obiekty plikowe (pliki i katalogi). Jej metody umożliwiają m.in. uzyskiwanie informacji o plikach i katalogach, jak również wykonywanie działań na systemie plikowym.
 
Wybrane metody klasy File
 booleancanRead()
          Czy plik może być czytany?
 booleancanWrite()
          Czy plik może być zapisywany?
 booleancreateNewFile()
          Tworzy nowy plik
static FilecreateTempFile(String prefix, String suffix)
         Tworzy nowy plik tymczasowy z nazwą wg wzorca
static FilecreateTempFile(String prefix, String suffix, File directory)
         Tworzy nowy plik tymczasowy z nazwą wg wzorca w podanym katalogu
 booleandelete()
          Usuwa plik lub katalog
 voiddeleteOnExit()
          Zaznacza plik do usunięcia po zakończeniu programu.
 booleanexists()
          Czy plik/katalog istnieje?
 StringgetName()
          Nazwa pliku lub katalogu
 StringgetParent()
          Katalog nadrzędny
 StringgetPath()
          Ścieżka
 booleanisDirectory()
          Czy to katalog?
 booleanisFile()
          Czy plik?
 booleanisHidden()
          Czy ukryty?
 longlastModified()
          Czas ostatniej modyfikacji
 longlength()
          Rozmiar
 String[]list()
          Lista nazw plików i katalogów w katalogu
 String[]list(FilenameFilter filter)
          Lista nazw plików wg wzorca; zob wykład 3
 File[]listFiles()
          Lista plików i katalogów
 File[]listFiles(FileFilter filter)
           Lista plików i katalogów
 File[]listFiles(FilenameFilter filter)
          Lista plików i katalogów
static File[]listRoots()
          Lista dostępnych "rootów" w systemie plikowym
 booleanmkdir()
          Tworzy katalog
 booleanmkdirs()
          Tworzy katalog i ew. niezbędne (niestniejące) katalogi nadrzędne
 booleanrenameTo(File dest)
          Renames the file denoted by this abstract pathname.
 booleansetLastModified(long time)
          Ustala czas ostatniej modyfikacji
 booleansetReadOnly()
          Zaznacza jako tylko od odczytu
 URItoURI()
          Tworzy obiekt klasy URI (Uniform Resource Identifier), reprezentujący ten obiekt plikowy 
 URLtoURL()
           Tworzy obiekt klasy URL (Uniform Resource Locator), reprezentujący ten obiekt plikowy 



5.13. Pliki o dostępie swobodnym

Klasa RandomAccessFile definiuje pliki o dostępie swobodnym, które mogą być otwarte z trybie "czytania" lub "czytania i pisania".  Swobodny dostęp oznacza dostęp do dowolnego bajtu danych bez potrzeby sekwencyjnego przetwarzania pliku od początku. 

Konstruktory klasy mają następującą postać:

    RandomAccessFile(String filename, String mode)
    RandomAccessFile(File file, String mode)

    gdzie mode oznacza jeden z następujących trybów otwarcia


Pliki o dostępie swobodnym mogą być traktowane jako ciągi bajtów. Bieżący bajt  do odczytu lub miejsce do zapisu określa specjalny wskaźnik pozycji w pliku (filePointer). Pozycję tę możemy zmieniać za pomoca metod seek() i skip(). Jest także zmieniana przy każdej operacji czytania lub pisania.
Do czytania/pisania służy wiele metod read... i write... , które pozwalają operować  na różnych rodzaajch danych odczytywanych z i zapisywanych do pliku (np. readDouble, readLine, writeInt itp.),

Pliki o dostępie swobodnym nie są strumieniami. Klasa RandomAccessFile nie należy więc do hierarchii klas strumieniowych




5.14. Nowe wejście-wyjście (NIO): przegląd

Niewątpliwie podstawowym motywem wprowadzenia nowych środków wejścia-wyjścia, zawartych w pakicie java.nio i jego podpakietach było zapewnienie zwiększenia efektywności działania programów w wysokim stopniu obciążonych operacjami wejścia-wyjścia o dużej częstotliwości.

Tradycyjne wejście-wyjście w Javie jest blokujące, tzn. wątek, który podejmuje próbę odczytania danych ze strumienia (metoda read()) jest blokowany, jeśli danych w strumieniu (jeszcze) nie ma. Zatem obsługa strumieni połączeń sieciowych (przez serwer) musiała być wykonywana poprzez wiele wątków, każdy z których obsługiwał strumień związany z jednym gniazdem (socket) i ew. był blokowany na tym strumieniu, czekający na dane. W takiej sytuacji przy dużej liczbie połączeń działa równolegle dużo wątków, a ponieważ liczba połaczeń zmienia się dynamicznie, to z dużą częstotliwością wiele wątków powstaje i umiera (przechodzi do stanu Dead). W sumie prowadzi to do problemów efektywnościowych, a także jest trudne w programowaniu, gdyż:
Podstawowym elementem nowego wejścia-wyjścia, odpowiadającym na te problemy,  jest koncepcja kanału.

Kanał reprezentuje otwarte połączenie do obiektu, który wykonuje jedną lub wiele różnych operacji wejścia-wyjścia. Takim obiektem może być urządzenie sprzętowe, plik, gniazdo sieciowe, a nawet komponent programu.


Gniazdo sieciowe (socket) jest punktem docelowym dwustronnej komunikacji dwóch programów działających równolegle w sieci
Kanały podłączone do gniazd sieciowych (np. klasa SocketChannel)  umożliwiają nieblokujące (asynchroniczne) wejście-wyjście.
Przy czytaniu z takiego kanału za pomoca metody read(...), wątek nie jest blokowany, gdy brak danych. Metoda read(...) natychmiast zwraca wynik - liczbę przeczytanych bajtów (jeśli nie ma jeszcze danych - to 0).
Umożliwia to odpytywanie (polling) - sprawdzanie co jakiś czas czy dane nadeszły. Wątek nie jest blokowany i może wykonywać inne zadania.

Schemat nieblokującego wejścia

SocketChannel socketChannel = SocketChannel.open(); // otwarcie kanału
socketChanel.connect(...)                           // podłączenie do gniazda
socketChannel.configureBlocking (false);            // tryb nieblokujący

...

while (true) {
   ...
   if (socketChannel.read (buffer) != 0) {  // czy są jakieś dane?
      processInput (buffer);                // tak - przetwórz je
   }
   else {                                   // nie - wykonuj inne czynności
     ...
   }
}
Uwaga: co to jest buffer? O tym za chwilę.

Jasne jest, że stanowi to alternatywę dla wielu wątków, z których każdy blokowany jest na czytaniu danych z kanału. Teraz jeden wątek może obsługiwać wiele kanałów bez blokowania przy braku danych.
Nazywa się to multipleksowaniem kanałów wejścia/wyjścia.

Ale samo nieblokowane wejście-wyjście nie wystarcza jeszcze, by prawidłowo rozwiązać problem obsługi wielu połączeń. "Ręczne" odpytywanie wielu kanałów ma wady:
Dlatego w java.nio wprowadzono mechanizm selektorów, który łączy ze sobą zalety nieblokującego odpytywania oraz natychmiastowej reakcji na dane po blokowaniu wątku na odczycie.

Dzięki nieblokującemu we/wy, a szczególnie selektorom:
Niewątpliwie przy nieblokującym wejściu-wyjściu można by to oprogramować w Javie i bez selektorów, ale użycie selektorów ma dwie podstawowe zalety:

O użyciu kanałów i selektorów do komunikacji w środowiskach sieciowych będziemy mogli powiedzieć więcej przy okazji omawiania programowania klient-serwer..

"Nowe wejście-wyjście" ma rownież szereg nie  związanych z programowaniem sieciowym, mających ogólniejsze znaczenie, własciwości.

Obecnie w Javie kanały mogą być połączone z gniazdami, plikami oraz dowolnymi strumieniami klas InputStream, OutputStream (te ostatnie kanały nazwiemy kanałami strumieniowymi).

Wszystkie takie kanały mają jedną wspólną właściwość: kanały wejściowe wprowadzają dane do  buforow bajtowych, a kanały wyjściowe wyprowadzają dane z tych buforów.
Bufor bajtowy przypomina nieco tablicę bajtów: jest to skończona sekwencja bajtów, na której można wykonywać pewne operacje (m.in. pobieranie i dodawanie); od zwykłej tablicy różni się przede wszystkim tym, że operacje na buforze są dostosowane do potrzeb wejścia-wyjścia
Zatem zapoznanie się z koncepcją buforów -  również jednego z elementów "nowego wejścia-wyjścia" - jest nieodzowne do korzystania z kanałów.
Bufory mogą być też stosowane samodzielnie i dostarczają pewnych nowych, ciekawych możliwości, które mogą być wykorzystywane niezależnie od kanałowego wejścia-wyjścia.

Zanim przejdziemy do bardziej konkretnego omawiania niektórych nowych możliwości, dostarczanych przez  kanały i bufory, warto rzucić okiem na syntezę tych możliwości (tablica).

KANAŁY

Kanały gniazdowe

Kanały plikowe
Kanały strumieniowe
uzyskanie kanału
metoda open() z klas SocketChannel,
ServerSocketChannel,
DatagramChannel
+ connect
Metoda
getChannel()
z klas:
FileInputStream
FileOutputStream
RandomAccessFile 
Metody statyczne klasy
Channels:
newChannel(InputStream)
newChannel(OutputStream)

Możliwości dostarczane przez kanały
nieblokujące wejście-wyjście
i możliwość stosowania selektorów

tak

nie

nie
atomistyczne czytanie rozprowadzające po wielu buforach (scattering read), atomistyczne pisanie gromadzące z wielu buforów (gathering write)

tak

tak

nie
mapowanie plików na pamięć

nie

tak

nie
bezpośrednie transfery kanałowe (np. jedno wywołanie kopiuje cały plik do innego w sposób bardzo efektywny)
tak, jeśli jednym z kanałów jest kanał plikowy
tak
tak, jeśli jednym z kanałów jest kanał plikowy
blokowanie (lock) dostępu do całego pliku   lub jego części

nie

tak

nie
czytanie, pisanie do/z buforów
(zob. możliwości buforów)

tak

tak

tak



Możliwości, dostarczane przez bufory
różne widoki buforów bajtowych (jako ciągu bajtów lub elementów wybranego typu pierwotnego)
przestawianie bajtów (możliwość wyboru i/lub łatwej zmiany  konwencj uporządkowania bajtów danych binarnych - czy bardziej znaczące częsci danej mają w pamięci mniejsze czy większe adresy)
kodowanie - dekodowanie danych w buforach znakowych przy uwzględnieniu wybranej strony kodowej (klasy Charset, CharsetDecoder i CharsetEncoder, operujące na buforach)
bufory bezpośednie (bufory takie  "opakowują" pamięć alokowaną poza JVM przez natywne środki platformy systemowej i umożliwiają m.in. wysoką efektywność operacji kanałowych, a także dają programiście możliwość dostępu z poziomu programu Javy do dowolnego obszaru pamięci, alokowanego w systemie - np. bezpośredniej pamięci graficznej)


Zwróćmy na koniec jeszcze raz uwagę, że "nowe wejście-wyjście" (NIO) nie zastępuje "starego" (klas strumieniowych). Generalnie klasy strumieniowe operują na wyższym poziomie abstrakcji, kanały zaś przeznaczone są przede wszystkim do zwiększenia efektywności operacji wejścia-wyjścia i ukierunkowane są raczej na niskopoziomowe operowanie na sekwencjach bajtów.

5.15. NIO: bufory

Bufor jest ciągłą, skończoną sekwencją elementów jednego z typów pierwotnych: byte, short, char, long, float, double


Odpowiednio do tego mamy różne klasy opisujące bufory np.

ByteBuffer - bufor bajtów (elementy typu byte),
IntBuffer  - bufor zawierający liczby calkowite typu int,
DoubleBuffer - bufor liczb typu double.

Oprócz tych klas, odpowiadających typom pierwotnym, istnieje rownież klasa MappedBuffer, za pomocą której możemy mieć dostęp do pliku mapowanego na pamięć.
Ogólne charakterystyki i funkcjonalność wszystkich klas buforowych określa klasa abstrakcyjna Buffer.

Rysunek przedstawia hierarchię klas buforowych.

r

Źródło:  Ron Hitchens, Java NIO, O'Reilly 2002
UWAGA: Kanały operują wyłącznie na buforach bajtowych.

Opakowanie danych tablicy przez bufor oznacza, że dane są te same - zarówno w buforze jak i tablicy, a operacje na buforze, zmieniające dane bufora zmieniają równocześnie dane w tablicy i vice versa
Bufory mogą być tworzone poprzez: Dane do buforów mogą być zapisywane za pomoca metod put(..) z klas buforowych oraz na skutek czytania z kanału (read(...) z klas kanałowych). Metody kanałowe zapisują do bufora  całe sekwencje bajtów.
Dane z buforów mogą być pobierane za pomocą metod get(...)  z klas buforowych oraz zapisywane do kanału (write()...) z klas kanałowych).

Metody put(...) i get(...) dzielą się na relatywne (zapisujące lub odczytujące bufor poczynając od jego bieżącej pozycji) i absolutne (zapisujące/odczytujące element na podanej pozycji w buforze).
Operacje relatywne mogą dotyczyć pojedynczego elementu lub ich sekwencji (zapisywanych do bufora z tablicy lub innego bufora, odczytywanych z bufora do tablicy).


Metody get(...) z klas buforowych
(typ oznacza byte, char, short, int, long, float, doble - w zależności od typu bufora).
  typ
get()
          Relatywna metoda get - zwraca bieżący element
 typBufferget(typ[] dst)
          Relatywna metoda get - zapisuje tablicę dst elementami bufora, poczynając od jego bieżacej pozycji.
 typBufferget(typ[] dst, int offset, int length)
          j.w. ale diotyczy części tablicy
typget(int index)
          Absolutna metoda get - zwraca element na pozycji index.

Metody put z klas buforowych
(typ oznacza byte, char, short, int, long, float, doble - w zależności od typu bufora).
 typBufferput(typ b)
          Relatywna metoda put - zapisuje element b  na bieżącej pozycji
 typBufferput(typ[] src)
          Relatywna metoda put - zapisuje do bufora tablicę elementów
 typBufferput(typ[] src, int offset, int length)
          j.w. - część tablicy
 typBufferput(typBuffer src)
          Relatywna metoda put - zapisuje zawartosc bufora src do bufora
 typBufferput(int index, typ b)
          Absolutna metoda put, zapisuje b na pozycji index
Uwaga: jeżeli bufor jest tylko do odczytu (read-only), to użycie metody put(...) spowodują powstanie wyjątku ReadOnlyBufferException. Bufor możemy uczynić "tylko do odczytu" za pomocą metod asReadOnly() z klas buforowych.


Każdy z buforów ma następujące (ważne, a czasem nieintuicyjne) charakterystyki: Informacje o tych charakterystykach można uzyskac za pomocą metod
Pojemność bufora nie może być zmieniona, ale pozycję i limit możemy sustawiać za pomocą metod
Metoda remaining() zwraca liczbę elementów znajdujących się pomiędzy pozycją a limitem.
Metoda hasRemaining() zwraca true, jeśli liczba ta jest większa od 0.


Każdy relatywny zapis do bufora zapisuje element bądź elementy do bufora poczynając od jego bieżącej pozycji i zmienia pozycję, tak by wskazywała na następny element (miejsce), który może być zapisany.
Każde relatywne czytanie z bufora czyta element bądź elementy, poczynajac od jego bieżącej pozycji i zmienia pozycję tak, by wskazywala na następny jeszcze nie odczytany element.

Uwaga:
Operacje absolutne nie zmieniają pozycji bufora.
Czytanie z bufora bądź zapis do bufora nie zmieniają limitu bufora.

Próba odczytu danych poza limitem spowoduje powstanie wyjątku BufferUnderflowException (brak danych w buforze), natomiast próba zapisu danych poza limit spoowoduje wyjątek BufferOverflowException (przepełnienie bufora).

Po zapisaniu danych do bufora, po to by można było je odczytać należy bufor przestawić.

Przestawienie bufora polega na wywolaniu metody flip(), która ustawia limit na bieżącą pozycję, po czym ustala pozycję na zero.

Jeżeli  po odczytaniu danych z bufora chcemy jeszcze raz przeczytać je od początku, powinniśmy bufor przewinąć. Służy temu metoda rewind(), która ustala pozcycję na zero, pozostawiając limit bez zmian


Zachowanie buforów i zmiany ich charakterystyk ilustruje poniższy program.

import java.nio.*;

class Buffers {

  static void say(String s) { System.out.println(s); }

  static void showParms(String msg, Buffer b) {
    say("Charakterystyki bufora - " + msg);
    say("capacity  :" + b.capacity());
    say("limit     :" + b.limit());
    say("position  :" + b.position());
    say("remaining :" + b.remaining());
  }

  public static void main(String args[]) {

    // alokacja bufora 10 bajtowego (inicjalnie warości elementów = 0)
    ByteBuffer b = ByteBuffer.allocate(10);
    showParms("Po utworzeniu", b);

    // Zapis dwóch bajtów do bufora
    b.put((byte) 7).put((byte) 9);
    showParms("Po dodaniu dwóch elementów", b);

    // Przestawienie bufora
    b.flip();
    showParms("Po przestawieniu", b);

    // Teraz możemy czytać wpisane dane
    say("Czytamy pierwszy element: " + b.get());
    showParms("Po pobraniu pierwszego elementu", b);
    say("Czytamy drugi element: " + b.get());
    showParms("Po pobraniu drugiego elementu", b);

    say("Czy możemy jeszcze czytać?");
    try {
      byte x = b.get();
    } catch (BufferUnderflowException exc) {
       say("No, nie - proszę spojrzeć na ostatni limit!");
    }

    // Jeszcze raz odczytajmy dane z bufora
    // w tym celu musimy go przewinąć
    b.rewind();
    showParms("Po przewinięciu", b);

    say("Czytanie wszystkiego, co wpisaliśmy");
    while (b.hasRemaining())
      say("Jest: " + b.get());
  }
}
który na wyjściu da:

Charakterystyki bufora - Po utworzeniu
capacity: 10
limit: 10
position: 0
remaining: 10
Charakterystyki bufora - Po dodaniu dwóch elementów
capacity: 10
limit: 10
position: 2
remaining: 8
Charakterystyki bufora - Po przestawieniu
capacity: 10
limit: 2
position: 0
remaining: 2
Czytamy pierwszy element: 7
Charakterystyki bufora - Po pobraniu pierwszego elementu
capacity: 10
limit: 2
position: 1
remaining: 1
Czytamy drugi element: 9
Charakterystyki bufora - Po pobraniu drugiego elementu
capacity: 10
limit: 2
position: 2
remaining: 0
Czy możemy jeszcze czytać?
No, nie - proszę spojrzeć na ostatni limit!
Charakterystyki bufora - Po przewinięciu
capacity: 10
limit: 2
position: 0
remaining: 2
Czytanie wszystkiego, co wpisaliśmy
Jest: 7
Jest: 9


Proszę uważnie przeanalizować działanie i wyniki tego programu.


Warto podkreślić, że nawigacja "po buforze" zawsze odbywa się w kontekście typu jego elementów.
Np.  w buforze typu IntBuffer elementami są liczby typu int, get zwraca element typu int, a  pozycja będzie się przesuwać tak, by wskazywać na następny element typu int (4 bajty).
Natomiast w buforze typu ByteBuffer pobieranie, zapisywanie i zmiany pozycji dotyczą pojedynczych bajtów.
Należy zwrócić szczególną uwagę na bufory bajtowe, bowiem

Bufory bajtpwe (typu ByteBuffer) odgrywają szczególną rolę, bowiem tylko one są używane w operacjach na kanalach.


5.16 NIO: kanały i bufory

Aby używać kanalów musimy importować nazwy klas z pakietu java.nio.channels.
Bufory wymagają importu java.nio.

Schematy użycia kanałów w operacjach wejścia-wyjścia przedstaiwono poniżej. Dla ustalenia uwagi posługujemy się wyłącznie kanałami plikowymi.

Zapis do  kanału
1. Alokacja bufora bajtowego

     ByteBuffer buf = ByteBuffer.allocate(N); // N - rozmiar bufora

2. Zapis danych do bufora (np. z użyciem metod put...)

3. Uzyskanie kanału klasy FileChannel "do zapisu"

Możemy go uzyskać za pomocą metod getChannel() z klas FileOutputStream oraz   RandomAccessFile (kanał "do zapisu i do odczytu", jeśli taki był tryb otwarcia pliku o dostępie swobodnym). Np.

    FileOutputStream out  = new FileOutputStream(...);
    FileChannel fc = out.getChannel();

4. Zapis bufora - użycie metod write z klasy FileChannel. Np.

   fc.write(buf);

5. Zamknięcie kanału:

   fc.close();

Zamknięty kanał pozostaje zamknięty. Metoda isOpen() pozwala stwierdzić czy kanał jest otwarty.




Czytanie z  kanału
1. Alokacja bufora bajtowego

     ByteBuffer buf = ByteBuffer.allocate(N); // N - rozmiar bufora

2. Uzyskanie kanału klasy FileChannel "do odczytu"

Możemy go uzyskać za pomocą metod getChannel() z klas FileInputStream oraz   RandomAccessFile (kanał "do zapisu i do odczytu", jesli taki był tryb otwarcia pliku o dostępie swobodnym). Np.

    FileInputStream out  = new FileInputStream(...);
    FileChannel fc = out.getChannel();

3. Czytanie do bufora - użycie metod read z klasy FileChannel. Np.

   fc.read(buf);

4. Zamknięcie kanału:

   fc.close();

Zamknięty kanał pozostaje zamknięty.

5. Określenie właściwej pozycji i limitu bufora po wczytaniu do niego danych.
    Np. poprzez przestawienie bufora:

    buf.flip();

6. Odczytanie danych z bufora za pomocą metod get...



Poniższy program ilustruje powyższy schemat. Komentarze szczegółowo omawiają użyte w nim konstrukcje. Proszę zwrócić na nie baczną uwagę.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

class SimpleChannel {

  String fname = "test.tmp";
  byte[] data = {1,2,3,4,5 };

  SimpleChannel() {
    try {
      writeChannel(fname, data);
      byte[] wynik = readChannel(fname);
      for (int i=0; i < wynik.length; i++) System.out.println(wynik[i]);
    } catch(Exception exc) {
        exc.printStackTrace();
        System.exit(1);
    }
  }

  void writeChannel(String fname, byte[] data) throws Exception {

    // Możemy utworzyć bufor przez opakowanie istniejącej tablicy
    ByteBuffer buf = ByteBuffer.wrap(data);

    FileOutputStream out = new FileOutputStream(fname);

    // Uzyakanie kanału
    FileChannel fc = out.getChannel();

    //Zapis
    fc.write(buf);
    fc.close();
  }

  byte[] readChannel(String fname) throws Exception {

    // Używamy obiektu klasy File
    // by dowiedzieć się jaki jest rozmiar pliku
    // i odpowiednio alokowac bufor
    File file = new File(fname);

    // Stworzenie strumienia na podstawie obiektu klasy File
    FileInputStream in = new FileInputStream(file);

    // Uzyskanie kanału
    FileChannel fc = in.getChannel();

    // Metoda size() z klasy FileChannel
    // zwraca long -rozmiar plku, do którego podlączony jest kanał
    int size = (int) fc.size();

    // Utworzenie bufora
    ByteBuffer buf = ByteBuffer.allocate(size);

    // Czytanie do bufora
    // nbytes - liczba przeczytanych bajtów
    int nbytes = fc.read(buf);
    fc.close();

    // Po przeczytaniu danych musimy bufor przestawić
    buf.flip();

    // Stworzenie tablicy na wynik czytania
    // jej rozmiar będzie określony przez liczbę przeczytanuych bajtów
    // którą możemy podać na dwa sposoby: poprzednie nbytes
    // lub uzyskując informację o liczbie jeszcze nieodczytanych bajtów z bufora

    byte[] wynik = new byte[buf.remaining()];
    buf.get(wynik);
    return wynik;
  }

  public static void main(String args[]) {
   new SimpleChannel();
  }
}   

W klasie FileChannel zdefiniowano wiele metod pozwalających na czytanie z - zapis do kanałów oraz pozycjonowanie i przewijanie plików, do których te kanały są podlączone.

5.17. Widoki buforów bajtowych

Jak już wiemy, w operacjach kanalowych używa się wyłącznie buforów bajtowych,.
Zapewniają one łatwy dostęp do elementów - bajtów.
Ale jak w  takim razie przetwarzać dane innych typów (niż bajty) pobierane z kanałów lub zapisywać takie dane do kanału?

Są po temu przynajmniej dwie możliwości.

Pierwsza polega na użyciu metod klasy ByteBuffer:

    ttt elt = getTtt()
    putTtt(Ttt elt)

które umożliwają zapisywanie i pobieranie danych typu ttt (short, int, long, float, double, char).

Drugi sposób - znacznie ciekawszy i przyjemniejszy - polega na użyciu widoków buforów bajtowych

Metoda klasy ByteBuffer:

    TttBuffer asTttBuffer()

gdzie: ttt - jeden z typów short, int, long, float, double, char

użyta wobec bufora bajtowego buf zwraca referencję do bufora klasy TttBuffer, co pozwala na działanie na danych pierwotnego bufora bajtowego buf za pomocą metod get i put klasy TttBuf czyli tak, jakby pierwotny bufor bajtowy zawierał elementy typu ttt.

Mówimy, że metoda ...asBuffer() zwraca widok bufora bajtowego jako bufora typu TttBuffer, gdyż nie następuej tu żadne kopiowanie danych i zmiany danych wykonywane za pośrednictwem obu obiektów - bufora pierwotnego oraz jego widoku dotyczą tego samego bufora.


Koncepcję widoku bufora bajtowego ilustruje poniższy program.

import java.nio.*;

class BufView1 {

  public static void main(String args[]) {

    final int SHORT_SIZE = 2;
    // Alokacja buforu bajtowego,
    // mogącego przechowywać do 10 liczb typu short
    ByteBuffer bb = ByteBuffer.allocate(10*SHORT_SIZE);

    // Widok na ten bofor jak na short-bufor
    ShortBuffer sb = bb.asShortBuffer();

    // Dodanie trzech liczb typu short
    short a = 1, b = 2, c = 3;
    sb.put(a).put(b).put(c);

    // Co wpisano do bufora? Na wydruku: 1, 2, 3
    sb.flip();
    while (sb.hasRemaining()) System.out.println(sb.get());

    // Operujemy teraz na nim za pomocą bufora bajtowego
    // zmieniając bajty na pozycji 1, 3 i 5.
    byte  x = 4, y = 5, z = 6;
    bb.put(1, x).put(3, y).put(5, z);

    // Co pokaże short-bufor? Na wydruku 4, 5, 6
    sb.rewind();
    while (sb.hasRemaining()) System.out.println(sb.get());
  }
}

Z widokami bufora bajtowego wiąże się jednak pewna pułapka.
Otóż, mimo, że bufor i jego widok dotyczą tych samych danych to jednak mamy dwa bufory (bufor bajtowy i jego widok jakiegoś wybranego typu - np. ShortBuffer ), a w tych buforach zmiany pozycji oraz limitu są niezalezne od siebie.

Pozycja i limit bufora bajtowego są niezależne od pozycji i limitu  jego widoku.


Gdybyśmy np. chcieli w poprzendim przykladzie po wpisaniu liczb short do bufora bajtowego (za pomocą jego widoku) pokazać kolejne bajty wszystkich wpisanych liczb, to moglibyśmy pojść błędnym tropem: po wpisaniu danych przestawić bufor short (flip) i sądząc, że pierwotny bufor bajtowy także dostosuje swoją pozycję i limit wypsiywać bajty w pętli dopóki hasRemaining() nie zwróci false.
Ilustruje to poniższy program (w którym używamy metody showParms zpoprzednich przyjładów dla pokazania charakterystyk bufora):

  public static void main(String args[]) {

    final int SHORT_SIZE = 2;
    ByteBuffer bb = ByteBuffer.allocate(10*SHORT_SIZE);
    ShortBuffer sb = bb.asShortBuffer();
    short a = 1, b = 2, c = 3;
    sb.put(a).put(b).put(c);
    showParms("bufor short - po dodaniu liczb", sb);
    showParms("bufor bajtowy - po dodaniu liczb", bb);

    sb.flip();
    showParms("bufor short - po przestawieniu", sb);
    showParms("bufor bajtowy - po flip bufora short", bb);
    System.out.print("Dane");
    while (bb.hasRemaining()) System.out.print(" " + bb.get());
  }
}
 Wydruk.

Charakterystyki bufora - bufor short - po dodaniu liczb
capacity: 10
limit: 10
position: 3
remaining: 7

Charakterystyki bufora - bufor bajtowy - po dodaniu liczb
capacity: 20
limit: 20
position: 0
remaining: 20

Charakterystyki bufora - bufor short - po przestawieniu
capacity: 10
limit: 3
position: 0
remaining: 3

Charakterystyki bufora - bufor bajtowy - po flip bufora short
capacity: 20
limit: 20
position: 0
remaining: 20
Dane 0 1 0 2 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0


Zwrócmy uwagę: dodanie elementów do widoku bufora bajtowego nie zmienia jego pozycji i limitu, zmienia tylko pozycję i limit w widoku, Rownież flip() widoku nie ma wpływu na te parametry bufora bajtowego. Program wypisuje cały bufor, bajt po bajcie, a nie bajty tych elementów, ktore zostały dodane.

Aby w tym przykładzie wypisac tylko bajty wpisanych danych, powinniścmy po przestawieniu widoku bufora zmienić odpowiednio limit bufora bajtowego:
    sb.flip();
    bb.limit(sb.limit()*SHORT_SIZE);
    System.out.print("Bajty wpisanych danych");
    while (bb.hasRemaining()) System.out.print(" " + bb.get());

Jednak przy czytaniu danych z kanałów nie ma takiej potrzeby. Dane są wczytywane do bufora bajtowego, a nie do jego widoku. Zatem należy przestawić bufor bajtowy, a dopiero potem stworzyć i posługiwać się jego widokiem. Jest to sensowne, bowiem widok bierze pod uwagę pozycję i limit bufora, na podstawie którego jest tworzony, odpowiednio ustawiając swoją pozycję i limit.

Co pokazuje poniższy program.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

class BuffChan {

  String fname = "testfile.tmp";

  // inicjacja danych testowych
  void init() throws Exception {
    double[] data = { 0.1, 0.2, 0.3 };
    DataOutputStream out = new DataOutputStream(
                             new FileOutputStream(fname)
                            );
    for (int i=0; i < data.length; i++) out.writeDouble(data[i]);
    out.close();

  }

  BuffChan() throws Exception {

    // inicjalcja danych testowych
    init();

    // utworzenie bufora
    ByteBuffer buf = ByteBuffer.allocate(1000); // nie wiemy ile, maks 100B

    // uzyskanie kanału
    FileChannel fcin = new FileInputStream(fname).getChannel();

    // czytanie z kanału do bufora
    fcin.read(buf);
    fcin.close();

    // przestawienie bufora bajtowego
    buf.flip();

    // Utworzenie widoku bufora
    DoubleBuffer dbuf = buf.asDoubleBuffer();

    // Wypisanie odczytanych danych
    while (dbuf.hasRemaining()) System.out.println(dbuf.get());
  }

  public static void main(String args[]) throws Exception {
    new BuffChan();
  }
}


5.18. NIO: bufory - uporządkowanie bajtów (endianess)

Przy czytaniu i zapisywaniu plików, tworzonych na różnych platformach sprzętowych,  powstaje problem uporządkowania (kolejności występowania) bajtów danych binarnych,
Wynika to z tego w jaki sposób dane binarne przechowywane są w pamięci (i jak zapisywane w rejestrach procesora).

Porządek bajtów w reprezentacji binarnej danych określa się terminem endianess.

Istnieją dwa różne porządki:

Ilustruje to rysunek, na którym pokazano porządek bajtów liczby typu int równej 1234567 (heksadecymalnie 0x12D687).

r

O tym jaki jest porządek bajtów w buforach możemy dowiedzieć się za pomocą metody order(), która zwraca referencję do obiektu klasy ByteOrder.
W przypadku buforów bajtowych (i tylko dla nich)  możemy zmienić porządek za pomocą wywołania metody order(ByteOrder) z argumentem będącym stałą statyczną z klasy ByteOrder:

Widoki bufora bajtowego mają ten porządek bajtów, który występował dla bufora bajtowego w momencie tworzenia widoku (możemy powiedzieć, że widok dziedziczy porządek swojego bufora,  bajtowego obowiązujący w chwili tworzenia widoku)


Ilustruje to poniższy program:

import java.nio.*;

class Endianness {

   static void show(int n) {
       String s = Integer.toHexString(n);
       int l = s.length();
       for (int i=l; i < 8; i++) s = '0' + s;
       System.out.println("Liczba " + n + " hex -> " + s.toUpperCase());
   }

   public static void main(String args[]) {
     int num = Integer.parseInt(args[0]);
     ByteBuffer buf = ByteBuffer.allocate(4);
     System.out.println(buf.order().toString());
     IntBuffer b1 = buf.asIntBuffer();
     System.out.println("Porządek b1 " + b1.order());
     b1.put(num);
     b1.flip();
     show(b1.get());
     buf.order(ByteOrder.LITTLE_ENDIAN);
     System.out.println("Porządek buf " + buf.order());
     System.out.println("Porządek b1 " + b1.order());
     b1.rewind();
     show(b1.get());
     System.out.println("Porządek buf " + buf.order());
     System.out.println("Porządek dziedzizcony " + buf.asIntBuffer().order());
     show(buf.asIntBuffer().get());
  }
}
który dla podanej jako argument liczby 987654321 wyprowadzi następujące wyniki:


BIG_ENDIAN
Porządek b1 BIG_ENDIAN
Liczba 987654321 hex -> 3ADE68B1
Porządek buf LITTLE_ENDIAN
Porządek b1 BIG_ENDIAN
Liczba 987654321 hex -> 3ADE68B1
Porządek buf LITTLE_ENDIAN
Porządek dziedzizcony LITTLE_ENDIAN
Liczba -1318527430 hex -> B168DE3A




5.19. NIO: bufory znakowe. Kodowanie i dekodowanie. 

Bufory znakowe (klasa CharBuffer) reprezentują sekwencje elementów typu char (znaki unikodu).
Mają one wszystkie właściwości buforów (o których już była mowa), ale dodatkowo zapewniają pewną specyficzną funkcjonalność.

Zauważmy, że sekwencje znaków są reprezentowane również przez obiekty klas String i StringBuffer.
W Javie 1.4 uogólniono więc pewne elementarne operacje na sekwencjach znaków, wprowadzając interfejs CharSequence . Interfej ten implementowany jest przez wszystkie klasy w/w klasy: String, StringBuffer i CharBuffer, dzięki czemu możliwe jest (do pewnego stopnia) uniwersalne operowanie na sekwencjach znaków, niezaleznie od tego, czy są one obiektami klas String, StringBuffer czy CharBuffer.

Metody interfejsu CharSequence podano w tablicy,
 charcharAt(int index)
          Zwraca znak na pozycji index
 intlength()
          Zwraca długość sekwencji znaków
 CharSequencesubSequence(int start, int end)
          Zwraca podsekwencję od znaku na pozycji start do znaku na pozycji end (wyłacznie)
 StringtoString()
          Zwraca referencję do nowego obiektu klasy String, zawierającego tę sekwencję znaków.

Używając tych metod można napisac uniwersalną metodę operującą na sekwncjach znaków, która jako argument może przyjmować referencję do obiektu dowolnej z klas: String, StringBUffer i CharBuffer (należy się spodziewać, że zetsaw metod interfejsu CharSequence zostanie w przyszłości wzbogacony, co rozszerzy możliwości pisania uniwersalnych fragmentów kodu). Już teraz zresztą CharSequence pełni istotną użyteczną rolę, bowiem metody klas pakietu java.util.regex, umozliwiające anlalizę łańcuchów znakowych za pomocą wyrażeń regularnych, operują na obiektach typu CharSequence.

Z punktu widzenia buforów znakowych, wprowadzenie interfejsu CharSequence ma dwie ważne konsekwencje:
            String txt = editor.getText();
            CharBuffer buf = CharBuffer.wrap(txt);

To miłe, że mamy takie bufory znakowe, ale przecież kanały mogą operowac wyłącznie na buforach bajtowych.
Przy kanałowym czytaniu i zapisie strumieni tekstowych powstaje więc problem kodowania i dekodowania informacji tekstowej.
Znany nam sposób kodowania-dekodowania za pomocą klas InputStreamReader i OutputStreamReader dotyczy tylko plików, a przy tym nie nadaje się do zastosowania przy polączeniach kanałowych, bowiem kanały plikowe można uzyskać tylko od obiektów klas FileInputStream, FileOutputStream i RandomAccessFile.

Dla rozwiązania problemu dekodowania - kodowania można,  co prawda, zastosować klasę String, jak pokazuje poniższy program (konwertujący pliki z jednej strony kodowej na inną).

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

class Ende1 {

  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 {
       FileChannel fcin = new FileInputStream(infile).getChannel();
       FileChannel fcout = new FileOutputStream(outfile).getChannel();
       ByteBuffer buf = ByteBuffer.allocate((int)fcin.size());

       // czytanie z kanału
       fcin.read(buf);

       // przeniesienie zawartości bufora do tablicy bytes
       buf.flip();
       byte[] bytes = new byte[buf.capacity()];
       buf.get(bytes);

       // dekodowanie - za pomocą konstruktora klasy String
       String txt = new String(bytes, in_enc);

       // enkodowanie za pomocą metody getBytes z klasy String
       // utworzenie nowego bufora dla kanału wyjściowego
       // zapis do pliku poprzez kanał
       bytes = txt.getBytes(out_enc);
       buf = ByteBuffer.wrap(bytes);
       fcout.write(buf);

       fcin.close();
       fcout.close();
    } catch (Exception e) {
        System.err.println(e);
        System.exit(1);
    }

  }
} 
ale jest to rozwiązanie dodatkowo obciążające pamięć operacyjną oraz - jak widać - mało wygodne w programowaniu.

W NIO rozwiązano problem kodowania-dekodowania za pomoca wprowadzenia nowych klas Charset, CharsetDecoder, CharsetEncoder w pakiecie java.nio.charset.


Dekodowanie-kodowanie za pomocą Charset polega na:
            Charset charset = Charset.forName("ISO-8895-2");

            ByteBuffer buf  = ...
            CharBuffer cbuf = charset.decode(buf);

            CharBuffer cbuf  = ...
            ByteBuffer buf = charset.encode(cbuf);

 
Informację o nazwach dostępnych w danej implementacji JVM stron kodowych można uzyskać za pomocą metody availCharsets() z klasy Charset.

Przykładowy program pokazuje zastosowanie tej procedury do konwersji plików z jednej strony kodowej na inną (z wykorzystaniem kanałów).

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;


class Ende2 {

  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 {
       FileChannel fcin = new FileInputStream(infile).getChannel();
       FileChannel fcout = new FileOutputStream(outfile).getChannel();
       ByteBuffer buf = ByteBuffer.allocate((int)fcin.size());

       // czytanie z kanału
       fcin.read(buf);

       // Strony kodowe
       Charset inCharset  = Charset.forName(in_enc),
               outCharset = Charset.forName(out_enc);

       // dekodowanie bufora bajtowego
       buf.flip();
       CharBuffer cbuf = inCharset.decode(buf);

       // enkodowanie bufora znakowego
       // i zapis do pliku poprzez kanał

       buf = outCharset.encode(cbuf);
       fcout.write(buf);

       fcin.close();
       fcout.close();
    } catch (Exception e) {
        System.err.println(e);
        System.exit(1);
    }
  }
}


5.20. NIO: operacje kanałowe na wielu buforach (scattering i gathering)

Kanały pozwalają za jednym odwołaniem czytać dane do wielu buforów (scatter read) lub zapisywać dane z wielu buforów (gather write).
Służą do tego metody read i write (z klas kanałowych), które jako argument mają tablice buforów.



Jest to bardzo użyteczna właściwość, pozwala bowiem w łatwiejszy sposób  programować uzyskiwanie przez kanały danych, mających określoną strukturę, w której można wyróżnić jakieś części o określonym rozmiarze.
Przykładem mogą być  pliki graficzne lub dane z nagłówkami komunikacyjnymi.

Program testujący czytanie do wielu buforów.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class ScatteringTest {

  static final String fname = "scatter.tst";  // nazwa pliku testowego

  public static void main(String[] args) throws Exception {

    // Zapisywanie danych testowych

    DataOutputStream out = new DataOutputStream(
    	                       new FileOutputStream(fname) );
    short[]  dat1 = { 1, 2, 3, };
    double[] dat2 = { 10.1, 10.2, 10.3 };
    for (int i=0; i < dat1.length; i++) out.writeShort(dat1[i]);
    for (int i=0; i < dat2.length; i++) out.writeDouble(dat2[i]);
    out.close();

    //-----------------------------------------------------------+
    // Odczytywanie danych testowych                             |
    //-----------------------------------------------------------+

    FileInputStream in = new FileInputStream(fname);

    // Uzyskanie kanału
    FileChannel channel = in.getChannel();

    // Tablica bajt-buforów
    final int SHORT_SIZE  = 2,  // ile bajtów ma short
              DOUBLE_SIZE = 8;  // ........... i double

    ByteBuffer[] buffers = { ByteBuffer.allocate(dat1.length*SHORT_SIZE),
                             ByteBuffer.allocate(dat2.length*DOUBLE_SIZE)
                           };
    // jedno czytanie z kanału zapisuje kilka buforów !
    long r = channel.read(buffers);

    System.out.println("Liczba bajtów przeczytanych do obu buforów: " + r);

    // Przed uzyskiwaniem danych z buforów - trzeba je przestawić!
    buffers[0].flip();
    buffers[1].flip();

    // Pierwssy bufor
    // Widok na bufor jako na zawierający liczby short
    ShortBuffer buf1 = buffers[0].asShortBuffer();
    System.out.println("Dane 1");
    while ( buf1.hasRemaining()) {
      short elt = buf1.get();
      System.out.println(elt);
    }

    // Drugi bufor
    // Widok na bufor jako na zawierający liczby double
    DoubleBuffer buf2 = buffers[1].asDoubleBuffer();
    System.out.println("Dane 2");
    while ( buf2.hasRemaining()) System.out.println(buf2.get());
  }
}
wyprowadzi następujące wyniki:
Liczba bajtów przeczytanych do obu buforów: 30
Dane 1
1
2
3
Dane 2
10.1
10.2
10.3


Również przy zapisywaniu danych przez kanały, możliwość jednorazowego zapisu z wielu buforów może być wielce wygodna. Np. w pewnych buforach możemy przygotować stałe fragmenty danych i już ich nie zmieniać, inne zaś bufory wypełniać dynamicznie zmieniającą się treścią.
Potrzebne będzie tylko jedno wywołanie metody write, by zapisać cały zestaw buforów (co oprócz wygody programistycznej ma również pozytywny wpływ na efektywność działania naszego programu).

Przykładowy program zapisuje kilka plików, każdy z których ma taki sam nagłówek i zakończenie. Częsć środkową wypełniają zmieniające się - od pliku do pliku - dane.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

class GatheringTest {

  public static void main(String[] args) throws Exception {

    // To będą stałe części każdego pliku
    String sHeader = "To jest nagłówek. Może być duży";
    String sFooter = "To jest zakończenie. Może być duże";

    // To będą dane, które się zmieniają od pliku do pliku
    byte[][] dane = { { 1, 2, 3},       // dane 1-go pliku
                     { 9, 10, 11, 12 }, // dane 2-go pliku
                     { 100, 101}        // dane 3-go pliku
                   };

    Charset charset = Charset.forName("windows-1250");
    ByteBuffer header = charset.encode(CharBuffer.wrap(sHeader)),
               footer = charset.encode(CharBuffer.wrap(sFooter));

    // Drugi element tablicy buforów będzie dynamicznie się zmienial
    // na razie = null
    ByteBuffer[] contents = { header, null, footer };
    for (int i = 0; i<dane.length; i++) {
      FileChannel fc = new FileOutputStream("plik"+i).getChannel();
      contents[1] = ByteBuffer.wrap(dane[i]);  // podstawienie zmiennych danych
      fc.write(contents);                      // zapis danych ze wszystkich buforów!
      fc.close();
      header.rewind();
      footer.rewind();
    }
  }


}

Czytanie i pisanie "po wielu buforach" od razu, nazywane także wektorowym wejściem-wyjściem (vectored I/O) jest atomistyczne nie tylko w tym sensie, że realizowane jest przez jedno odwołanie z poziomu programu. Na nowoczesnych platformach systemowych daje ono dużą poprawę efektywności, bowiem następuje znaczna redukcja liczby niskopoziomowych (na poziomie jądra systemu) operacji we/wy oraz ich optymalizacja (w skrajnym przypadku do atomistycznej - pojedynczej - calkowicie zoptymalizowanej niskopoziomowej operacji we/wy).


5.21. NIO: mapowanie plików

Mapowanie pliku polega na odzwierciedleniu całości lub części pliku w pamięci, bezpośrednio dostępnej dla programu.


Mapowanie pliku w NIO uzyskujemy poprzez uzyskanie kanału podłączonego do pliku (niech oznacza go zmienna channel), a następnie użycie metody map z klasy FileChannel. Metoda ta zwraca referencję do obiektu klasy MappedByteBuffer (pochodnej od Buffer), który jest bajtowym buforem bezpośrednim (direct), zawierającym bajty pliku lub jego wybranego segmentu.


Mapowanie pliku

    FileChannel channel = ....; // podłączenie kanału do pliku
    MappedByteBuffer buf = channel.map( tryb, pozycja, rozmiar);

gdzie:


Po zmapowaniu pliku, na uzyskanym buforze bajtowym i jego widokach możemy wykonywać operacje, znane nam z klas buforowych, a operacje te de-facto będą operacjami na zawartości pliku.

Dostępne tryby dzialania zależą od trybów podłączenia (otwarcia) kanału.
Tryb READ_ONLY może być użyty tylko dla kanałów otwartych do czytania, natomiast tryby READ_WRITE oraz PRIVATE wymagają kanałów otwartych w trybie odczytu-zapisu.

Odpowiedniość między trybami kanałów, a rodzajami plików/strumieni pokazuje poniższa tablica.

Tryb mapowania
Tryb otwarcia kanału
Rodzaj pliku,
do którego podłączamy kanał
metodą getChannel()
READ_ONLY
tylko do odczytu
FileInputStream
RandomAccesFile otwarty w trybie tylko do odczytu ("r")
READ_WRITE
PRIVATE

do odczytu-zapisu

RandomAccesFile otwarty w trybie pisania-czytania ("rw")


Mapowanie plików może być wygodne z punktu widzenia programowania (działanie na pliku jak na buforze i jego widokach - lub w niektórych przypadkach jak na tablicy).

Ale nie tylko to jest zaletą mapowania plików.
Otóż uzyskiwany bufor mapujący (obiekt klasy MappedByteBuffer)  jest buforem bezpośrednim, czyli alokowanym poza przestrzenią adresową programu .
W nowoczesnych systemach operacyjnych może on prawie w ogóle nie zajmować pamięci systemu!  Oczywiście jakieś fragmenty pamięci są używane dla prowadzenia odwzorowania, ale generalnie strony pamięci wirtualnej nie są zajęte. Obszarem stronicowanie jest sam plik!


Sprawia to, iż wiele programów naraz może mapować ten sam duży plik i działać na nim, nawet jeśli sumaryczna wielkość wszystkich odwzorowań przekracza wielkość dostępnej pamięci operacyjnej.
Z drugiej strony, to że bufory dla mapowanych plików są bezpośrednie (czyli alokowane gdzieś poza programami) sprawia, że mapowane pliki mogą stanowić wygodną realizację pamięci dzielonej (przez różne procesy) i sposobem na komunikwoanie się tych procesów.

Zobaczmy przykłady.
Pierwszy program ilustracyjny zapisuje najpierw plik testowy liczbami całkowitymi, następnie uzyskuje kanał i mapuje plik, po czym wykonuje zmiany w tym pliku, operując wylącznie na mapującym buforze.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

class MapFiles1 {

  String fname = "test";

  public MapFiles1() throws Exception {
    init();         // inicjacja pliku testowego
    mapAndChange(); // mapowanie i zmiana danych pliku
    checkResult();  // sprawdzenie wyników
  }

  void init() throws IOException {
    int[] data = { 10, 11, 12, 13 };
    DataOutputStream out = new DataOutputStream(
                               new FileOutputStream(fname)
                            );
    for (int i=0; i<data.length; i++) out.writeInt(data[i]);
    out.close();
  }

  void mapAndChange() throws IOException {

    // Aby dokonywać zmian musimy przyłączyć kanal
    // do pliku otwartego w trybie "read-write"
    RandomAccessFile file =  new RandomAccessFile(fname, "rw");
    FileChannel channel = file.getChannel();

    // Mapowanie pliku
    MappedByteBuffer buf;
    buf  = channel.map(
                FileChannel.MapMode.READ_WRITE,  // tryb "odczyt-zapis"
                                             0,  // od początku pliku
                            (int)channel.size()  // cały plik
            );

    // Uzyskujemy widok na bufor = zmapowany plik
    IntBuffer ibuf = buf.asIntBuffer();

    // Dla ciekawości: jakie charakterystyki widoku
    System.out.println(ibuf + " --- Direct: " +  ibuf.isDirect());

    int i = 0;
    while (ibuf.hasRemaining()) {
      int num = ibuf.get();       // pobieramy kolejny element
      ibuf.put(i++, num * 10);    // zapisujemy jego wartość*10 na jego pozycji    }
    }

    // Zapewnia, że zmiany na pewno zostaną odzwierciedlone w pliku
    buf.force();

    channel.close();
  }

  void checkResult() throws IOException {
    DataInputStream in = null;
    try {
      in = new DataInputStream(new FileInputStream(fname));
      while(true) System.out.println(in.readInt());
    } catch(EOFException exc) {
        return;
    } finally {
        in.close();
    }
  }

  public static void main(String[] args) throws Exception  {
    new MapFiles1();
  }
}
Program wyprowadzi:
java.nio.DirectIntBufferS[pos=0 lim=4 cap=4] --- Direct: true
100
110
120
130


Na przykładzie tego programu warto zwrócić uwagę na następujące kwestie:
Drugi program ma trochę bardziej praktyczne zastosowanie. Pozwala on "w miejscu" zmienić kodowanie podanego jako argument pliku z Windows1250 na ISO-8859-2 jednocześnie zamieniając wszystkie litery na duże.
Przy okazji zobaczymy, że:
Oto tekst programu. Proszę go przetestowac na jakimś samodzielnie utworzonym pliku html, zapisanym w stronie kodowej Windows 1250).
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

class MapFiles2 {

  public static void main(String[] args) throws Exception  {
    Charset inCharset = Charset.forName("windows-1250"),
           outCharset = Charset.forName("ISO-8859-2");

    RandomAccessFile file = new RandomAccessFile(args[0], "rw");
    FileChannel fc = file.getChannel();

    // Mapowanie pliku
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
                                  0, (int) fc.size());

    // Utworzenia bufora znakowego ze zdekodowanymi znakami
    // z bufora bajtowego (mapujące plik). Konwersja: win1250->unicode
    CharBuffer cbuf = inCharset.decode(mbb);

    // Okazuje się, że ten nowo utworzony bufor opakowuje tablicę
    // zatem możemy ją uzyskać i działać na jej elementach
    // to dzialanie oznacza dzialanie na elementach bufora
    char[] chArr = cbuf.array();
    for (int i=0; i < chArr.length; i++)
      chArr[i] = Character.toUpperCase(chArr[i]);

    // Po dekodowaniu bufor bajtowy musi być przewinięty do początku
    // aby koder (zob. dalej) mógł w nim zapisywać kodowane dane
    mbb.rewind();

    // Utworzenie kodera, zamieniającego Unicode na wyjściową stronę kodową
    CharsetEncoder encoder = outCharset.newEncoder();

    // Koder zapisuje istniejący bufor mbb (ten który mapuje plik)
    // ostatni argument - true oznacza zakończenie pracy kodera na tym wywołaniu
    encoder.encode(cbuf, mbb, true);
    fc.close();
  }
}



5.22. NIO: bezpośrednie transfery kanałowe

Bezpośredni transfer kanałowy polega na przeslaniu wszystkich danych jednego kanału do drugiego kanału za pomocą jednego odwołania z poziomu programu, z możliwym (w zależności od systemu operacyjnego i platformy sprzętowej) pominięciem systemowych operacji wejścia-wyjścia, a zrealizowanym jako bardzo szybki trarnsfer wykonywany przez sam sprzęt ze wsparciem ze strony jądra systemu
 

Obecnie w NIO bezpośrednie transfery kanałowe dotyczą tarnsferów do/z plików.
Klasa FileChannel dostarcza dwóch metod, które pozwalają na:

Bezpośrednie transfery kanałowe w systemach, które w swoim jądrze zapewniają wsparcie takich transferów, mogą być bardzo szybkie i efektywne.
W innych systemach (jak np. Windows), poprawa efektywności (w porównaniu z tradycyjnymi sposobami kopiowania danych) może być różna, nawet dość nieznaczna.

Zawsze jednak zaletą stosowania metod transfer...  z klasy FileChannel pozostaje możliwośc uniknięcia potrzeby programowania (w kliku wierszach, ale zawsze to trochę pracy) procedur kopiowania danych z/do plików.

Oto przykładowy program, porównujący dwa sposoby kopiowania plików: za pomocą bezpośredniego transferu kanałowego i poprzez buforowane strumienie.
import java.nio.*;
import java.nio.channels.*;
import java.io.*;

class DirectTransfer {

  String inFileName;
  String outFileName;

  DirectTransfer(String infn, String outfn) throws Exception {
    inFileName  = infn;
    outFileName = outfn;
    directTransfer();
    copyByStream();
  }


  void directTransfer() throws Exception {

    FileInputStream in = new FileInputStream(inFileName);
    FileOutputStream out =  new FileOutputStream(outFileName);
    FileChannel fcin = in.getChannel();
    FileChannel fcout = out.getChannel();
    long size = fcin.size();
    System.out.println("Copying file " + size + "B.");

    long start =  System.currentTimeMillis();

    // Bezpośredni transfer
    fcout.transferFrom(fcin, 0, size);

    long end = System.currentTimeMillis();

    System.out.println("Direct transfer time " + (end - start));

   }


  final int BUFSIZE = 5000000;
  void copyByStream() throws Exception  {

    FileInputStream fin = new FileInputStream(inFileName);
    BufferedInputStream in = new BufferedInputStream(fin, BUFSIZE );
    FileOutputStream fout = new FileOutputStream(outFileName);
    BufferedOutputStream out = new BufferedOutputStream(fout, BUFSIZE);

    byte[] b = new byte[BUFSIZE];
    long start =  System.currentTimeMillis();
    while (true) {
      int n = in.read(b);
      if (n == -1) break;
      out.write(b, 0, n);
    }
    in.close();
    out.close();

    long end = System.currentTimeMillis();
    System.out.println("Stream time " + (end - start));
  }


   public static void main(String[] args) throws Exception {
     new DirectTransfer(args[0], args[1]);
   }
}
Możliwy wynik:

Copying file 51793936B.
Direct transfer time 2470
Stream time 6150




5.23. Podsumowanie


Po lekturze tego rozdziału wiemy już bardzo dużo o operacjach-wejścia w Javie.
Dzięki abstrakcji strumieniowej możemy pisać bardzo uniwersalne i elastyczne programy, zaś poznanie nowych środków wejścia-wyjścia (NIO) daje możliwość pisania programów, działających bardzo efektywnie przy dużym obciążeniu operacjami wejścia-wyjścia.


5.24. Zadania i ćwiczenia

  1. Stworzyć klasę, która pozwala na zliczanie wystąpień w podanym strumieniu podanego napisu.
  2. Jak 1, ale użyć kanałów plikowych
  3. Napisać program, w którym występują dwa wątki. Wątek 1 przegląda system plikowy w poszukiwaniiu plików html. Nazwy odnalezionych plików przekazuje za pomocą potoku drugiemu wątkowi, który zmienia kodowanie w przekazanych plikach z Windows-1250 na ISO-8859-2 (jesli taka konieczność występuje).
  4. Napisać dwa programy, które wymieniają dane za pomocą zmapowanego pliku.