Advanced   Java   Services Buffers Back Next Up Home


Streaming IO

Das Konzept der Streamklassen und der Reader/Writerklassen ist Streaming. Es wird byte für byte gelesen oder geschrieben. Dieses Konzept ermöglicht einfache Lese- und Schreibmethoden und relativ einfaches Codieren. Der Preis dafür ist eine mäßige Performance. Verbessern kann man diese durch Vorauslesen/schreiben und puffern der Daten. Dazu gubt es die BufferedXxx Klassen. Mit ihnen erzielt man eine deutliche Performanceverbesserung. Für kleine Dateien ist die Performance dadurch sogar gut bis sehr gut.


Block IO

Dieses Konzept arbeitet mit Datenblöcken. Daten werden in Blöcken gelesen oder geschrieben. Dazu muß es eigene Klassen geben, da dies mit den Streamklassen nicht möglich ist. Diese werden Channels genannt. Die aus dem Channel gelesenen Blöcke müssen dann in geeigneten Behältern gespeichert werden. Diese Behälter sind verallgemeinerte Arrays, die sog. Buffers. Vorteil der Buffers ist, daß sie Methoden bereitstellen, mit denen man ihren Zustand abfragen oder manipulieren kann. Die man mit Channels und Buffers hantieren muß, ist die Codierung aufwendiger als beim Streaming. Für große Datenmengen bietet dieses Konzept aber die bessere Performance.


Buffers und Channels

Mit der Javaversion 1.4 wurde das Block IO Konzept mit Hilfe der Bufferklassen und der Channelinterfaces realisiert. Bevor wir mit Channels arbeiten, ist es gut zu wissen, wie man mit Buffers umgeht. Die wichtigste Bufferklasse ist ByteBuffer. Diese Klasse ist die am meisten verwendete Klasse. Da die anderen Bufferklassen nach dem gleichen Schema funktionieren reicht es ByteBuffer zu betrachten. Wir beginnen mit einer Übersicht über die Bufferklassen.


Die Hierarchie der Bufferklassen



java-buffer-hierarchie.jpg


Die Methoden der Klasse Buffer
Modifier and Type  Method and Description
abstract Object array()
Returns the array that backs this buffer  (optional operation).
abstract int arrayOffset()
Returns the offset within this buffer's backing array of the first element of the buffer  (optional operation).
int capacity()
Returns this buffer's capacity.
Buffer clear()
Clears this buffer.
Buffer flip()
Flips this buffer.
abstract boolean hasArray()
Tells whether or not this buffer is backed by an accessible array.
boolean hasRemaining()
Tells whether there are any elements between the current position and the limit.
abstract boolean isDirect()
Tells whether or not this buffer is direct.
abstract boolean isReadOnly()
Tells whether or not this buffer is read-only.
int limit()
Returns this buffer's limit.
Buffer limit(int newLimit)
Sets this buffer's limit.
Buffer mark()
Sets this buffer's mark at its position.
int position()
Returns this buffer's position.
Buffer position(int newPosition)
Sets this buffer's position.
int remaining()
Returns the number of elements between the current position and the limit.
Buffer reset()
Resets this buffer's position to the previously-marked position.
Buffer rewind()
Rewinds this buffer.


Einen ByteBuffer anlegen

Am einfachsten ist es, die statische Methode allocate zu verwenden, die jeder Implementierung der abstrakten Klasse Buffer zur Verfügung stellt.

int size = 1024;
ByteBuffer buffer = ByteBuffer.allocate(size);

Mit Hilfe der statischen Methode wrap() kann man ein bestehendes Array als backing Array für einen Buffer verwenden.

int size = 1024;
byte[] arr = new byte[size];
ByteBuffer buffer2 = ByteBuffer.wrap(arr);

Ergebnis ist jedesmal eine Instanz vom Typ java.nio.HeapByteBuffer, einer packageprivaten Klasse in java.nio.

Die Klasse ByteBuffer ist die einzige Bufferimplementierung, mit der man einen Buffer über die Methode allocateDirect() anlegen kann. In der API heißt es hierzu:

The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.

int size = 1024;
ByteBuffer dBuffer = ByteBuffer.allocateDirect(size);

Ergebnis ist hier eine Instanz vom Typ java.nio.DirectByteBuffer, einer packageprivaten Klasse in java.nio.


Anfangszustand eines ByteBuffers

Der Zustand eines Buffers läßt sich mit den drei Parametern position, limit und capacity beschreiben. Der im Bild dargestellte Buffer hat eine Kapazität von 16 bytes, wie bei einem Array ist also der höchste Index capacity -1.

niobuffer-anfangszustand.jpg

Die Bedeutung von limit wird sich weiter unten klären.


Schreiben in einen ByteBuffer

Schreiben bedeutet Daten von irgendwoher auslesen und in den Buffer schreiben. Eine Möglichkeit dafür bietet die Methode put().

int size = 128;
ByteBuffer buffer = ByteBuffer.allocate(size);
String st = "Understanding Buffers";
buffer.put( st.getBytes() );

Die toString()-Methode von ByteBuffer ist so überladen, daß sie den Zustand des ByteBuffer anzeigt. Hier ergibt ein Konsolausgabe:

java.nio.HeapByteBuffer[pos=21 lim=128 cap=128]

position ist also ein Positionszeiger, dessen Wert angibt, wieviele byte eingelesen wurden und der dadurch auf das erste freie byte zeigt.

niobuffer-written.jpg

Da jeder Buffer eine begrenzte Kapazität hat, kann es passieren, daß man mehr speichern will als die Kapazität des Buffers zuläßt. Der Buffer beantwortet den Versuch mit einer BufferOverflowException. In diesem Fall werden keine Daten in den Buffer geschrieben, der Buffer behält seinen alten Zustand.

try
{
   String st3 = " And now the next Question: let's try what happens if we want to fill more in the buffer as his capacity says ?";
   buffer.put( st3.getBytes() );
   System.out.println(buffer);
}
catch(BufferOverflowException ex)
{
   System.out.println(ex);
   System.out.println(buffer);
   // state of the buffer will not be changed
}

Auslesen von Daten aus einem ByteBuffer

Will man Daten aus dem Buffer auslesen, muß man wissen, wo der Positionszeiger steht. Dann muß man den Positionszeiger auf 0 setzen um die Daten vom Anfang her lesen zu können. Hier hilft die Methode flip(). Sie setzt einen eigenen Zeiger namens limit auf den Wert von position und schiebt anschließend position auf den Anfang.

buffer.flip();  // lim -> pos und pos -> 0

niobuffer-flip.jpg

Nun stellen wir ein Array bereit, in das wir die ausgelesenen Daten speichern wollen und übergeben dies der Methode get(). get() liest genau alle Daten bis zum limit.

int limit = buffer.limit();
byte[] arr = new byte[limit];
// filling the bytearray
buffer.get(arr);

Und schiebt den Positionszeiger zum Limit.

niobuffer-after-read.jpg


Erneutes Auslesen mit rewind()

Die Daten im Buffer werden nicht gelöscht. Man kann sie mit der Methode rewind() erneut auslesen. Dazu setzt rewind() den Positionszeiger wieder auf den Anfang!

niobuffer-flip.jpg


Buffer wieder zum Schreiben bereitmachen mit clear()

Will man neue Daten in den Buffer bringen, so müssen die Zeiger auf den Ausgangszustand zurückgesetzt werden. Die alten Daten werden dabei nicht gelöscht, sondern erst bei einem neuen Schreibvorgang überschrieben.

buffer.clear();  // lim -> capacity, pos -> 0

niobuffer-anfangszustand.jpg

Valid XHTML 1.0 Strict top Back Next Up Home