Zuvor haben wir die IO API (Input/Output Application Programming Interface) und das Paket java.io kennengelernt , dessen Klassen hauptsächlich für die Arbeit mit Streams in Java gedacht sind. Der Schlüssel hier ist das Konzept eines Streams .

Heute beginnen wir mit der Betrachtung der NIO API (New Input/Output).

Der Hauptunterschied zwischen den beiden I/O-Ansätzen besteht darin, dass die IO-API streamorientiert ist, während die NIO-API pufferorientiert ist. Die wichtigsten Konzepte, die es zu verstehen gilt, sind Puffer und Kanäle .

Was ist ein Puffer und was ist ein Kanal?

Ein Kanal ist ein logisches Portal, über das Daten ein- und ausgehen, während ein Puffer die Quelle oder das Ziel dieser übertragenen Daten ist. Bei der Ausgabe werden die Daten, die Sie senden möchten, in einen Puffer gelegt, und der Puffer leitet die Daten an den Kanal weiter. Während der Eingabe werden die Daten vom Kanal in den Puffer gelegt.

Mit anderen Worten:

  • Ein Puffer ist einfach ein Speicherblock, in den wir Informationen schreiben und aus dem wir Informationen lesen können.
  • Ein Kanal ist ein Gateway, das Zugriff auf E/A-Geräte wie Dateien oder Sockets ermöglicht.

Kanäle sind den Streams im java.io-Paket sehr ähnlich. Alle Daten, die irgendwohin gehen (oder von irgendwoher kommen), müssen ein Kanalobjekt passieren. Im Allgemeinen erhalten Sie für die Verwendung des NIO-Systems einen Kanal zu einer I/O-Entität und einen Puffer zum Speichern von Daten. Anschließend arbeiten Sie mit dem Puffer und geben je nach Bedarf Daten ein oder aus.

Sie können sich in einem Puffer vorwärts und rückwärts bewegen, also durch den Puffer „laufen“, was in Streams nicht möglich ist. Dies gibt mehr Flexibilität bei der Datenverarbeitung. In der Standardbibliothek werden Puffer durch die abstrakte Klasse Buffer und mehrere ihrer Nachkommen dargestellt:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • FloatBuffer
  • DoubleBuffer
  • LongBuffer

Der Hauptunterschied zwischen den Unterklassen ist der Datentyp, den sie speichern – Bytes , Ints , Longs und andere primitive Datentypen.

Puffereigenschaften

Ein Puffer hat vier Haupteigenschaften. Dies sind Kapazität, Grenze, Position und Markierung.

Die Kapazität ist die maximale Menge an Daten/Bytes, die im Puffer gespeichert werden kann. Die Kapazität eines Puffers kann nicht geändert werden . Sobald ein Puffer voll ist, muss er geleert werden, bevor mehr in ihn geschrieben werden kann.

Im Schreibmodus entspricht die Grenze eines Puffers seiner Kapazität und gibt die maximale Datenmenge an, die in den Puffer geschrieben werden kann. Im Lesemodus bezieht sich das Limit eines Puffers auf die maximale Datenmenge, die aus dem Puffer gelesen werden kann.

Die Position gibt die aktuelle Position des Cursors im Puffer an. Beim Erstellen des Puffers wird er zunächst auf 0 gesetzt. Mit anderen Worten, es ist der Index des nächsten Elements, das gelesen oder geschrieben werden soll.

Die Markierung dient zum Speichern einer Cursorposition. Wenn wir einen Puffer manipulieren, ändert sich die Cursorposition ständig, wir können ihn jedoch jederzeit an die zuvor markierte Position zurücksetzen.

Methoden zum Arbeiten mit einem Puffer

Schauen wir uns nun die wichtigsten Methoden an, mit denen wir mit unserem Puffer (Speicherblock) arbeiten können, um Daten in und aus Kanälen zu lesen und zu schreiben.

  1. allocate(int Capacity) – diese Methode wird verwendet, um einen neuen Puffer mit der angegebenen Kapazität zuzuweisen. Die allocate()- Methode löst eine IllegalArgumentException aus , wenn die übergebene Kapazität eine negative Ganzzahl ist.

  2. Capacity() gibt die Kapazität des aktuellen Puffers zurück .

  3. position() gibt die aktuelle Cursorposition zurück. Lese- und Schreibvorgänge bewegen den Cursor an das Ende des Puffers. Der Rückgabewert ist immer kleiner oder gleich dem Grenzwert.

  4. limit() gibt das Limit des aktuellen Puffers zurück.

  5. mark() dient zum Markieren (Speichern) der aktuellen Cursorposition.

  6. reset() setzt den Cursor an die zuvor markierte (gespeicherte) Position zurück.

  7. clear() setzt die Position auf Null und setzt die Grenze auf die Kapazität. Diese Methode löscht die Daten im Puffer nicht . Es werden lediglich die Position, das Limit und die Markierung neu initialisiert.

  8. flip() schaltet den Puffer vom Schreibmodus in den Lesemodus. Außerdem wird der Grenzwert auf die aktuelle Position gesetzt und die Position dann wieder auf Null gesetzt.

  9. read() – Die Lesemethode des Kanals wird zum Schreiben von Daten vom Kanal in den Puffer verwendet, während die Methode put() des Puffers zum Schreiben von Daten in den Puffer verwendet wird.

  10. write() – Die Methode write des Kanals wird zum Schreiben von Daten aus dem Puffer in den Kanal verwendet, während die Methode get() des Puffers zum Lesen von Daten aus dem Puffer verwendet wird.

  11. rewind() spult den Puffer zurück. Diese Methode wird verwendet, wenn Sie den Puffer erneut lesen müssen. Sie setzt die Position auf Null und ändert den Grenzwert nicht.

Und nun ein paar Worte zu den Kanälen.

Die wichtigsten Kanalimplementierungen in Java NIO sind die folgenden Klassen:

  1. FileChannel – Ein Kanal zum Lesen und Schreiben von Daten aus/in eine Datei.

  2. DatagramChannel – Diese Klasse liest und schreibt Daten über das Netzwerk über UDP (User Datagram Protocol).

  3. SocketChannel – Ein Kanal zum Lesen und Schreiben von Daten über das Netzwerk über TCP (Transmission Control Protocol).

  4. ServerSocketChannel – Ein Kanal zum Lesen und Schreiben von Daten über TCP-Verbindungen, genau wie ein Webserver. Für jede eingehende Verbindung wird ein SocketChannel erstellt.

Üben

Es ist Zeit, ein paar Codezeilen zu schreiben. Lassen Sie uns zunächst die Datei lesen und ihren Inhalt auf der Konsole anzeigen und dann eine Zeichenfolge in die Datei schreiben.

Der Code enthält viele Kommentare – ich hoffe, sie helfen Ihnen zu verstehen, wie alles funktioniert:


// Create a RandomAccessFile object, passing in the file path
// and a string that says the file will be opened for reading and writing
try (RandomAccessFile randomAccessFile = new RandomAccessFile("text.txt", "rw");
    // Get an instance of the FileChannel class
    FileChannel channel = randomAccessFile.getChannel();
) {
// Our file is small, so we'll read it in one go   
// Create a buffer of the required size based on the size of our channel
   ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
   // Read data will be put into a StringBuilder
   StringBuilder builder = new StringBuilder();
   // Write data from the channel to the buffer
   channel.read(byteBuffer);
   // Switch the buffer from write mode to read mode
   byteBuffer.flip();
   // In a loop, write data from the buffer to the StringBuilder
   while (byteBuffer.hasRemaining()) {
       builder.append((char) byteBuffer.get());
   }
   // Display the contents of the StringBuilder on the console
   System.out.println(builder);
 
   // Now let's continue our program and write data from a string to the file
   // Create a string with arbitrary text
   String someText = "Hello, Amigo!!!!!";
   // Create a new buffer for writing,
   // but let the channel remain the same, because we're going to the same file
   // In other words, we can use one channel for both reading and writing to a file
   // Create a buffer specifically for our string — convert the string into an array and get its length
   ByteBuffer byteBuffer2 = ByteBuffer.allocate(someText.getBytes().length);
   // Write our string to the buffer
   byteBuffer2.put(someText.getBytes());
   // Switch the buffer from write mode to read mode
   // so that the channel can read from the buffer and write our string to the file
   byteBuffer2.flip();
   // The channel reads the information from the buffer and writes it to our file
   channel.write(byteBuffer2);
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}

Probieren Sie die NIO-API aus – Sie werden es lieben!