NIO

K.1. 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:


"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.

K.2. 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
 typBuffer get(typ[] dst)
          Relatywna metoda get - zapisuje tablicę dst elementami bufora, poczynając od jego bieżacej pozycji.
 typBuffer get(typ[] dst, int offset, int length)
          j.w. ale diotyczy części tablicy
typ get(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).
 typBuffer put(typ b)
          Relatywna metoda put - zapisuje element b  na bieżącej pozycji
 typBuffer put(typ[] src)
          Relatywna metoda put - zapisuje do bufora tablicę elementów
 typBuffer put(typ[] src, int offset, int length)
          j.w. - część tablicy
 typBuffer put(typBuffer src)
          Relatywna metoda put - zapisuje zawartosc bufora src do bufora
 typBuffer put(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 ustawiać 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.


K.3 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ę tu 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 {


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

    // 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.

K.4. 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 wypisywać bajty w pętli dopóki hasRemaining() nie zwróci false.
Ilustruje to poniższy program (w którym używamy metody showParms zpoprzednich przykł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();
  }
}


K.5. 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




K.6. 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.
Od Javy 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,
 char charAt(int index)
          Zwraca znak na pozycji index
 int length()
          Zwraca długość sekwencji znaków
 CharSequence subSequence(int start, int end)
          Zwraca podsekwencję od znaku na pozycji start do znaku na pozycji end (wyłacznie)
 String toString()
          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 sekwencjach 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);
    }
  }
}


K.7. 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).


K.8. 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();
  }
}



K.9. 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