In diesem Artikel zeige ich dir anhand eines Beispiels, wie der Java ByteBuffer
funktioniert und was die Methoden flip()
und compact()
genau machen.
Folgende Fragen wird der Artikel beantworten:
- Was ist ein
ByteBuffer
, und wofür benötigt man ihn? - Wie erzeugt man einen
ByteBuffer
? - Was bedeuten die Werte
position
,limit
undcapacity
? - Wie schreibt man in den ByteBuffer, wie liest man daraus?
- Was genau machen die Methoden
flip()
undcompact()
?
Los geht's!
Was ist ein ByteBuffer, und wofür benötigt man ihn?
Einen ByteBuffer
benötigst du, um mit einem sogenannten "Channel" Daten in eine Datei, einen Socket oder eine andere I/O-Komponente zu schreiben bzw. daraus zu lesen.
(In diesem Artikel geht es vorranging um den ByteBuffer
selbst. Wie du mit dem ByteBuffer
und einem FileChannel
Dateien schreibst und liest, erfährst du im FileChannel-Artikel des Dateien-Tutorials.)
Ein ByteBuffer
ist ein Wrapper um ein Byte-Array und bietet Methoden zum komfortablen Schreiben in und Lesen aus dem Byte-Array. Der ByteBuffer
speichert intern die Schreib-/Lese-Position und ein sogenanntes "Limit".
Was das genau bedeutet, erfährst du im folgenden Beispiel – Schritt-für-Schritt.
Den im Zuge dieses Artikels geschriebenen Programmcode findest du in diesem GitHub-Repository.
Einen ByteBuffer erzeugen
Als erstes muss ein ByteBuffer
mit einer vorgegebenen Größe (englisch: "capacity") angelegt werden. Hierfür gibt es zwei Methoden:
ByteBuffer.allocate(int capacity)
ByteBuffer.allocateDirect(int capacity)
Mit dem Parameter capacity
wird die Größe des Buffers in Byte angegeben.
Die Methode allocate()
legt den Buffer im Java Heap Memory an, wo er nach der Verwendung durch den Garbage Collector wieder entfernt wird.
allocateDirect()
hingegen legt den Buffer im nativen Speicher an, also außerhalb des Heaps. Das hat den Vorteil, dass Schreib- und Leseoperationen schneller ausgeführt werden, da die entsprechenden Betriebssystem-Operationen direkt auf diesen Speicherbereich zugreifen können und die Daten nicht zunächst zwischen dem Java-Heap und dem Betriebssystem ausgetauscht werden müssen. Der Nachteil dieser Methode sind höhere Allokations- und Deallokationskosten.
Wir legen wie folgt einen ByteBuffer
mit einer Größe von 1.000 Bytes an:
var buffer = ByteBuffer.allocate(1000);
Code-Sprache: Java (java)
Danach schauen wir uns die Kennzahlen des Buffers an – nämlich position
, limit
und capacity
.
Da wir diese Kennzahlen im Verlauf des Beispiels wiederholt ausgeben werden, legen wir dafür eine printMetrics
-Methode an:
private static void printMetrics(ByteBuffer buffer) {
System.out.printf("position = %4d, limit = %4d, capacity = %4d%n",
buffer.position(), buffer.limit(), buffer.capacity());
}
Code-Sprache: Java (java)
Nach dem Anlegen des ByteBuffers sehen wir folgende Ausgabe:
position = 0, limit = 1000, capacity = 1000
Code-Sprache: Klartext (plaintext)
Hier eine grafische Darstellung, damit du dir den Buffer besser vorstellen kannst. Der gelbe Bereich ist leer und kann im folgenden gefüllt werden.
ByteBuffer-Position, -Limit und -Capacity
Die ausgegebenen Werte bedeuten im Einzelnen:
position
ist die Schreib-/Leseposition. Diese ist bei einem neuen Buffer immer 0.limit
hat zwei Bedeutungen: Wenn wir in den Buffer schreiben, kennzeichnetlimit
die Position, bis zu der wir schreiben können. Wenn wir aus dem Buffer lesen, zeigtlimit
, bis zu welcher Position der Buffer Daten enthält. Initial ist einByteBuffer
immer im Schreibmodus undlimit
gleichtcapacity
– den leeren Buffer können wir bis zum Ende beschreiben.capacity
bezeichnet die Größe des Buffers. Diese entspricht den 1.000 Bytes, die wir bei derallocate()
-Methode angegebenen haben. Sie wird sich während der Lebenszeit des Buffers nicht mehr ändern.
Der ByteBuffer-Schreib-Lese-Zyklus
Ein vollständiger Schreib-Lese-Zyklus besteht aus den Schritten put()
, flip()
, get()
und compact()
. Diese werden wir uns in den folgenden Abschnitten anschauen.
Schreiben in den ByteBuffer mit put()
Zum Schreiben in den ByteBuffer
gibt es diverse put()
-Methoden, um einzelne Bytes, ein Byte-Array oder auch andere primitive Datentypen (wie char, double, float, int, long, short) in den Buffer zu schreiben.
In unserem Beispiel schreiben wir 100 mal den Wert 1 in den Buffer und schauen uns danach erneut die Buffer-Kennzahlen an:
for (int i = 0; i < 100; i++) {
buffer.put((byte) 1);
}
printMetrics(buffer);
Code-Sprache: Java (java)
Wenn wir das Programm laufen lassen, sehen wir folgende Ausgabe:
position = 100, limit = 1000, capacity = 1000
Code-Sprache: Klartext (plaintext)
Die Position hat sich also um 100 Bytes nach rechts verschoben; der Buffer sieht nun wie folgt aus:
Als nächstes schreiben wir 200 mal eine Zwei in den Buffer. Dieses Mal verwenden wir dazu eine andere Methode: Wir füllen zunächst ein Byte-Array, kopieren dieses in den Buffer, und geben wieder die Metriken aus:
byte[] twos = new byte[200];
Arrays.fill(twos, (byte) 2);
buffer.put(twos);
printMetrics(buffer);
Code-Sprache: Java (java)
Wir sehen nun:
position = 300, limit = 1000, capacity = 1000
Code-Sprache: Klartext (plaintext)
Die Position hat sich um weitere 200 Bytes nach rechts verschoben; der Buffer sieht wie folgt aus:
Umschalten in den Lesemodus mit Buffer.flip()
Zum Lesen aus dem Buffer gibt es entsprechende get()
-Methoden. Diese werden bspw. beim Schreiben in einen Channel durch Channel.write(buffer)
aufgerufen.
Da position
nicht nur die Schreib-, sondern auch die Leseposition anzeigt, müssen wir position
zurück auf 0 setzen.
Gleichzeitig setzen wir limit auf 300, um zu signalisieren, dass aus dem Buffer maximal 300 Bytes gelesen werden können.
Im Programmcode machen wir das wie folgt:
buffer.limit(buffer.position());
buffer.position(0);
Code-Sprache: Java (java)
Da diese zwei Zeilen bei jedem Umschalten vom Schreib- in den Lesemodus benötigt werden, gibt es eine ByteBuffer
-Methode, die genau dasselbe für uns tut:
buffer.flip();
Code-Sprache: Java (java)
Ein Aufruf von printMetrics()
zeigt uns nun folgende Werte:
position = 0, limit = 300, capacity = 1000
Code-Sprache: Klartext (plaintext)
Der Positions-Zeiger ist also an den Anfang des Buffers zurückgesprungen, und limit
zeigt auf das Ende des gefüllten Bereichs:
Damit ist der Buffer bereit, um gelesen zu werden.
Lesen aus dem ByteBuffer mit get()
Nehmen wir an, der Channel, in den wir schreiben wollen, kann momentan nur 200 der 300 Bytes aufnehmen. Das können wir simulieren wir, in dem wir der ByteBuffer.get()
-Methode ein 200 Byte großes Byte-Array übergeben, in das der Buffer seine Daten schreiben soll:
buffer.get(new byte[200]);
Code-Sprache: Java (java)
printMetrics()
zeigt uns nun folgendes an:
position = 200, limit = 300, capacity = 1000
Code-Sprache: Klartext (plaintext)
Die Leseposition ist um 200 Bytes nach rechts gerückt – also ans Ende der gelesenen Daten, bzw. an den Anfang der noch nicht gelesenen Daten:
Umschalten in den Schreibmodus – wie man es nicht macht
Um jetzt wieder in den Buffer zu schreiben, könnte man folgenden Fehler begehen: Man setzt position
auf das Ende der Daten, also 300, und limit
wieder auf 1.000. Damit sind wir bei exakt dem Zustand, in dem wir nach dem Schreiben der Einsen und Zweien waren:
Nehmen wir an, wir würden jetzt 300 weitere Bytes in den Buffer schreiben. Der Buffer sähe danach wie folgt aus:
Wenn wir nun mit flip()
zurück in den Lesemodus schalten würden, dann wäre position
wieder auf 0:
Jetzt würden wir allerdings die ersten, bereits gelesenen, 200 Bytes noch einmal lesen.
Diese Vorgehensweise ist also falsch. Wie man es richtig macht, erklärt der folgende Abschnitt.
Umschalten in den Schreibmodus mit Buffer.compact()
Stattdessen müssen wir beim Umschalten in den Schreibmodus wie folgt vorgehen:
- Wir berechnen die Anzahl der verbleibenden Bytes:
remaining = limit - position
, im Beispiel ergibt das 100. - Wir schieben die verbleibenden Bytes an den Anfang des Buffers.
- Wir setzen die Schreibposition ans Ende der nach links verschobenen Bytes, im Beispiel also auf 100.
- Wir setzen
limit
auf das Ende des Buffers.
Auch hierfür stellt ByteBuffer
eine Methode zur Verfügung:
buffer.compact();
Code-Sprache: Java (java)
Nach dem Aufruf von compact()
zeigt uns printMetrics()
folgendes an:
position = 100, limit = 1000, capacity = 1000
Code-Sprache: Klartext (plaintext)
In der Grafik sieht der compact()
-Vorgang wie folgt aus:
Der nächste Zyklus
Jetzt können wir die nächsten 300 Bytes in den Buffer schreiben:
byte[] threes = new byte[300];
Arrays.fill(threes, (byte) 3);
buffer.put(threes);
Code-Sprache: Java (java)
printMetrics()
zeigt uns jetzt folgende Werte an:
position = 400, limit = 1000, capacity = 1000
Code-Sprache: Klartext (plaintext)
Nach dem Schreiben der Dreien ist position
um 300 Bytes nach rechts gerückt:
Jetzt können wir problemlos mit flip()
wieder in den Lesemodus umschalten:
buffer.flip();
Code-Sprache: Java (java)
Ein letzter Aufruf von printMetrics()
gibt folgende Werte aus:
position = 0, limit = 400, capacity = 1000
Code-Sprache: Klartext (plaintext)
Die Leseposition steht am Anfang des Buffers, dort wo die verschobenen 100 Zweien liegen. Wir können nun also an genau der Position weiterlesen, an der wir vorhin aufgehört haben.
Wenn wir es richtig machen, lesen wir also keine Daten doppelt :)
Zusammenfassung
Dieser Artikel hat die Funktionsweise des Java ByteBuffer
s und dessen Methoden flip()
und compact()
anhand eines Beispiels erklärt.
Wenn dir der Artikel geholfen hat den ByteBuffer
besser zu verstehen, dann teile ihn gerne über einen der Share-Buttons unten, und hinterlasse mir einen Kommentar.
Möchtest du informiert werden, wenn neue Artikel auf HappyCoders.eu veröffentlicht werden? Dann klicke hier, um dich für den HappyCoders.eu-Newsletter anzumelden.