6.5Wahlfreier Zugriff mit SeekableByteChannel und ByteBuffer *
Für den wahlfreien Zugriff auf Bytes in Dateien bietet Java seit Version 1.0 die Klasse RandomAccessFile. Eine moderne Alternative für wahlfreie Ein-/Ausgabe bietet SeekableByteChannel. Der Typ steht für einen so genannten Channel; diese repräsentieren eine offene Verbindung mit einem Datenkanal. Mit einem Path ist so ein SeekableByteChannel leicht erfragt, und dann lässt sich ein Dateizeiger positionieren und lassen sich Daten lesen und schreiben. Gegenüber RandomAccessFile ist wegen einer hardwarenahen Implementierung eine bessere Geschwindigkeit möglich, sonst bietet ein SeekableByteChannel keine großartigen Vorzüge, er ist sogar etwas umständlicher in der Nutzung.
6.5.1SeekableByteChannel
Der SeekableByteChannel deklariert Operationen zum Lesen und Schreiben von Daten und zur Positionierung des Dateizeigers.
extends ByteChannel
int read(ByteBuffer dst) throws IOException
int write(ByteBuffer src) throws IOException
long size() throws IOException
long position() throws IOException
SeekableByteChannel position(long newPosition) throws IOException
SeekableByteChannel truncate(long size) throws IOException
Die Methoden close() und isOpen() kommen aus Channel hinzu.
Es fällt auf, und das ist einer der großen Unterschiede zu RandomAccessFile, dass SeekableByteChannel kein byte-Feld oder einzelne Bytes liest oder schreibt, sondern mit ByteBuffer einen ganz eigenen Typ erwartet.
6.5.2ByteBuffer
Ein ByteBuffer ist einem Byte-Feld sehr ähnlich; seine maximale Größe wird vorher festgelegt und kann später nicht dynamisch wachsen. Ist ein ByteBuffer angelegt, so können über einen Index die einzelnen Bytes gelesen und geschrieben werden, zum Beispiel mit byte get() oder put(byte) relativ zur letzten Position – oder mit byte get(int index) und put(int index, byte b) absolut. Der wirkliche Unterschied ist aber, dass Java zwei verschiedene Arten von ByteBuffer-Implementierungen bietet (ByteBuffer ist eine abstrakte Klasse):
Nicht direkteByteBuffer sind wie byte[]-Felder, also Java-Objekte, die auf dem Heap Platz einnehmen.
Bei einem direktenByteBuffer versucht Java, einen Speicherbereich vom Betriebssystem zu bekommen. Während die nicht direkten ByteBuffer und byte-Arrays auf dem Heap leben und der normalen automatischen Speicherbereinigung unterworfen sind, sollten die direkten ByteBuffer vom Betriebssystem verwaltet werden. Im Idealfall sind dadurch hohe Ein-/Ausgabegeschwindigkeiten möglich, denn mit direkten ByteBuffern kann sich das Betriebssystem Kopieroperationen zwischen nativen internen Puffern und Java-Puffern sparen.
Die Methoden auf direkten oder nicht direkten ByteBuffern sind identisch. Insbesondere gilt das für das Speichern aller Pufferzustände: die Position, ein Limit und eine Kapazität. Diesen Eigenschaften wollen wir aber in der Insel nicht nachgehen.
6.5.3Beispiel mit Path + SeekableByteChannel + ByteBuffer
Das folgende Beispiel fasst alles zusammen: Von einem Path wird über newByteChannel ein SeekableByteChannel erfragt. Anschließend leiten wir aus einer Zeichenkette über das byte[] einen nicht direkten ByteBuffer ab und schreiben diesen in den SeekableByteChannel, sodass später die Datei Kurt Cobain.txt einen ASCII-Text enthält.
Listing 6.34com/tutego/insel/nio2/SeekableByteChannelDemo.java, main()
StandardOpenOption.CREATE,
StandardOpenOption.WRITE ) ) {
String s = "Drugs are bad for you. ";
ByteBuffer byteBuffer = ByteBuffer.wrap( s.getBytes() );
raf.write( byteBuffer );
raf.write( ByteBuffer.wrap( "They will f*ck you up.".getBytes() ) );
raf.position( 34 );
raf.write( ByteBuffer.wrap( new byte[]{'u'} ) );
}
Das Beispiel zeigt, dass mit ByteBuffer.wrap(byte[]) aus dem Byte-Feld des Strings ein nicht direkter Buffer angelegt wird, den write(ByteBuffer) dann in den Kanal schreibt.
Abbildung 6.11Die Datei im Hex-Editor
Nur zum Testen schreiben wir ASCII-Zeichen, was aber im »echten Leben« eher nicht der Fall sein wird, denn wir müssen hier die korrekten Zeichenkodierungen beachten. Auch für sequenzielle Schreiboperationen ist der SeekableByteChannel eher weniger komfortabel – dennoch ist der Einsatz von Kanälen nicht per se falsch. Im nächsten Kapitel werden die Ströme vorgestellt, mit denen das Schreiben, insbesondere von Textdokumenten, viel einfacher wird.
6.5.4FileChannel
Die Schnittstelle SeekableByteChannel gibt Operationen an, um die aktuelle Position auszulesen und den Positionszeiger neu zu setzen und über ByteBuffer Bytes und Bytefolgen zu lesen und zu schreiben. SeekableByteChannel ist dabei nicht an Dateien gebunden und enthält keine Informationen zu Dateipfaden oder sonstigen tiefer liegenden Schichten. Und da Path grundsätzlich ein Pfad auf alles Mögliche sein kann, etwa auf ein BLOB in der Datenbank, liefert newByteChannel(…) eine Rückgabe mindestens von Typ SeekableByteChannel – und damit erst einmal keine Möglichkeiten, um dateispezifische Operationen vorzunehmen.
Wird allerdings newByteChannel(…) auf einem Pfad aufgerufen, der eine Datei vom Dateisystem repräsentiert, so ist die Rückgabe nicht einfach nur ein SeekableByteChannel, sondern der Untertyp FileChannel.[ 80 ](Den Typ FileChannel gibt es in Java schon länger, nämlich seit Java 1.4. Und vor Java 7 lieferten die Methoden getChannel() von FileInputStream, FileOutputStream und RandomAccessFile den FileChannel.) Ein Typecast ist daher möglich:
FileChannel channel = (FileChannel) Files.newByteChannel( p, options );
Da FileChannel die Schnittstelle SeekableByteChannel implementiert, bietet natürlich FileChannel alle Methoden zum Lesen, Schreiben und Positionieren. Zusätzlich bietet FileChannel aber Methoden, die explizit an Dateien gebunden sind. Drei Methoden fallen sofort auf:
lock(…): Sperrt die Datei (oder Dateiteile) für andere, soweit es das Betriebssystem unterstützt.
force(…): Updates werden sofort materialisiert, das heißt auf das Dateisystem übertragen.
map(…): Blendet die Datei, oder einen Teil der Datei, in den Speicher ein.
Die Methode map(FileChannel.MapMode, long, long) ist besonders interessant. Damit kann ein FileChannel auf einen ByteBuffer abgebildet werden, sodass unsere Lese-/Schreiboperationen auf dem ByteBuffer direkt aus der Datei kommen oder direkt in die Datei gehen. Das Betriebssystem versucht sein Bestes, die Operationen zu optimieren und geeignete Blöcke der Datei in den Speicher zu laden. Java und das Betriebssystem kooperieren also eng, um die Operationen so schnell wie möglich und mit wenigen Kopieroperationen zwischen den internen Puffern des Dateisystems und den Java-Puffern durchzuführen.
Das folgende Beispiel bezieht im ersten Schritt über newByteChannel(…) den FileChannel. Anschließend bildet die Methode map(…) die gesamte Datei auf einen MappedByteBuffer ab, der ein ByteBuffer ist, wie wir ihn im letzten Beispiel schon kennengelernt haben. Wir könnten nun Methoden auf dem ByteBuffer aufrufen und die Bytes auslesen, doch hier gehen wir etwas anders vor: Die Bytes des ByteBuffer konvertiert ein CharsetDecoder von ASCII in Java-Unicode; das Ergebnis ist ein CharBuffer. Den CharBuffer laufen wir ab und geben die Zeichen auf der Konsole aus:
Listing 6.35com/tutego/insel/nio2/FileChannelDemo.java, main()
try ( FileChannel fileChannel = (FileChannel) Files.newByteChannel( p,
StandardOpenOption.READ ) ) {
ByteBuffer byteBuffer = fileChannel.map( FileChannel.MapMode.READ_ONLY, 0,
fileChannel.size() );
CharsetDecoder decoder = StandardCharsets.ISO_8859_1.newDecoder();
CharBuffer charBuffer = decoder.decode( byteBuffer );
while ( charBuffer.hasRemaining() )
System.out.print( charBuffer.get() );
}
Beim FileChannel gilt das Gleiche wie beim SeekableByteChannel. Sequenzieller Lese- oder Schreibzugriff wird am einfachsten über die Stromklassen realisiert. Sie werden im folgenden Kapitel vorgestellt.