W3docs

Java NIO Überblick

Einführung in Java NIO und NIO.2 — Channels, Buffer, Selektoren und das java.nio.file-Paket.

Die fünfzehn Kapitel vor diesem behandelten java.io — Streams, Reader/Writer, File, Serializable. Diese API ist Javas ursprüngliches I/O-System und wird noch immer intensiv genutzt. NIO ist die Familie von APIs, die Java später hinzugefügt hat, um das abzudecken, was java.io nicht konnte. Sie besteht aus zwei Teilen, die ein Paketpräfix teilen, aber sonst wenig gemeinsam haben:

  • NIO (Java 1.4, 2002) — java.nio.* — Channels, Buffer, Selektoren. Eine andere Form von I/O: bytebasiert, optional nicht-blockierend, konzipiert für Hochdurchsatz-Server.
  • NIO.2 (Java 7, 2011) — java.nio.file.* — die Klassen Path, Files, FileSystem und WatchService. Ein benutzerfreundlicherer Ersatz für java.io.File und ein Ort für Dateisystemfunktionen, die java.io nie hatte (symbolische Links, erweiterte Attribute, asynchrone Datei-I/O, Verzeichnisüberwachung).

Du hast seit Beginn dieses Teils Teile von NIO.2 verwendet: Path, Files.newBufferedReader, Files.newInputStream stammen alle aus java.nio.file. Dieses Kapitel zoomt heraus und zeigt, wo diese Bausteine hineinpassen und wofür der Rest des Pakets gedacht ist.

Stream vs. Channel: zwei verschiedene Formen

InputStream.read() gibt ein Byte zurück. OutputStream.write(int) schreibt ein Byte. Das gedankliche Modell ist eine Ein-Byte-pro-Schritt-Pipe. Gepufferte Dekoratoren machen es schnell, aber die Abstraktion ist sequenziell und unidirektional.

Ein Channel (java.nio.channels.Channel) ist bidirektional, bytebasiert und unterstützt Operationen, die InputStream nicht ausdrücken kann:

  • Lesen in und Schreiben aus einem ByteBuffer — nicht aus einem byte[].
  • Memory-Mapping eines Dateibereichs in den RAM und Lesen/Schreiben als Buffer.
  • Scatter eines Lesevorgangs in mehrere Buffer (Header → einer, Nutzdaten → ein anderer).
  • Gather eines Schreibvorgangs aus mehreren Buffern (einzelner write() erzeugt eine zusammenhängende Ausgabe).
  • Einen Channel als nicht-blockierend markieren und einen Selector Tausende davon auf einem Thread multiplexen lassen.

Der Preis dafür ist Ausführlichkeit. Channel-Code liest und schreibt durch einen ByteBuffer mit expliziten flip()- und position()-Aufrufen; java.io verbirgt all das hinter read(byte[]). Für typisches Dateilesen bevorzuge die java.io/Files-APIs. Wechsle zu Channels, wenn du eine der Channel-exklusiven Funktionen benötigst.

// channel-shaped read into a 1 KB buffer
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
  ByteBuffer buf = ByteBuffer.allocate(1024);
  int n = ch.read(buf);                              // fills the buffer; updates position
  buf.flip();                                        // switch to "read what was just written"
  while (buf.hasRemaining()) {
    process(buf.get());
  }
}

Der flip()-Schritt ist der Moment, in dem man lernt, dass ByteBuffer seinen eigenen kleinen Zustandsautomaten hat.

ByteBuffer: Position, Limit, Kapazität

Ein ByteBuffer ist ein byte[] mit fester Größe (oder ein Stück Off-Heap-Speicher) plus drei Indizes:

  • position — das nächste zu lesende oder zu schreibende Byte.
  • limit — der Index hinter dem letzten Byte, das du berühren darfst.
  • capacity — die feste Größe des Buffers; kann sich nicht ändern.
0 ─────── position ─────── limit ─────── capacity
   (consumed)   (active region)   (untouchable / empty)

Der Buffer befindet sich konventionsgemäß in einem von zwei Modi:

  • Schreibmodus (Standard): du fügst put(byte)s ein. position rückt vor; limit == capacity.
  • Lesemodus: du holst get() Bytes heraus. position rückt vor; limit ist dort, wo du aufgehört hast zu schreiben.

flip() wechselt vom Schreib- in den Lesemodus: es setzt limit = position (markiert, wo die Daten enden) und setzt position = 0 zurück (beginnt vom Anfang zu lesen). clear() wechselt zurück in den Schreibmodus (position = 0, limit = capacity). Fehler hier sind die häufigste Ursache für die Frustration „Ich lese null Bytes; warum?".

Off-Heap-Buffer (ByteBuffer.allocateDirect(n)) umgehen den JVM-Heap und lassen das Betriebssystem sie direkt lesen/schreiben, ohne eine zusätzliche Kopie. Sie sind langsamer bei der Zuweisung, schneller beim I/O und die richtige Wahl nur für I/O-Code auf dem kritischen Pfad.

Selektoren: ein Thread, viele Channels

Vor virtuellen Threads (Java 21) bedeutete das Bedienen von Tausenden gleichzeitiger Netzwerkverbindungen in Java entweder Tausende von Betriebssystem-Threads (einer pro Verbindung — teuer) oder einen einzelnen Thread, der mit einem Selector multiplext:

Selector sel = Selector.open();
serverChannel.register(sel, SelectionKey.OP_ACCEPT);
while (true) {
  sel.select();                                       // blocks until any channel is ready
  for (SelectionKey k : sel.selectedKeys()) {
    if (k.isAcceptable()) accept(k);
    if (k.isReadable())   read(k);
  }
}

Das Betriebssystem benachrichtigt die JVM, wenn ein registrierter Channel Fortschritt machen kann; die JVM übergibt dir die bereite Menge; du führst ein nicht-blockierendes Lesen oder Schreiben durch und kehrst zu select() zurück. Der Framework-Code unter Netty, gRPC und Spring WebFlux hat genau diese Form.

Mit virtuellen Threads (Thread.startVirtualThread(...)) skaliert das einfachere „ein Thread pro Anfrage"-Muster auf dieselben Verbindungszahlen ohne die Selector-Choreografie — virtuelle Threads blockieren bei blockierendem I/O im Wesentlichen kostenlos. Für neuen Anwendungscode auf Java 21+ ist die Selektor-Schleife zunehmend ein Bibliotheksthema; du schreibst sie normalerweise nicht von Hand. Für Bibliothekscode und Pre-Loom-JVMs ist es das Standardmuster.

java.nio.file: die moderne Datei-API

Das ist die Hälfte von NIO, die du in alltäglichem Code verwenden wirst. Sie ersetzt java.io.File und die meisten dateibezogenen Teile von java.io:

java.iojava.nio.fileGrund für den Ersatz
FilePathUnveränderlich, betriebssystemunabhängig, keine eingebauten I/O-Methoden
File.list()Files.list(Path), Files.walk(Path)Stream<Path>; schließbar; beachtet symbolische Links
new FileInputStream(...)Files.newInputStream(path)Zeichensatz-bewusste Varianten für Text; eine einheitliche Open-API
file.delete() gibt bei Fehler false zurückFiles.delete(path) wirft IOExceptionFehler sind sichtbar, nicht still
kein ÄquivalentFiles.walkFileTree, WatchService, symbolischer Link-API, Dateiattribut-ViewsFähigkeiten, die java.io nie hatte

Die beiden kommenden Kapitel behandeln Path und Files im Detail. Die Faustregel: Für Dateiarbeit in Java 2024+ greife zu java.nio.file. java.io.File ist noch vorhanden, weil alter Code ihn verwendet, aber neuer Code sollte standardmäßig Path verwenden.

Ein durchgearbeitetes Beispiel: Datei via Channel und Buffer hin- und herkopieren

Das folgende Programm kopiert eine kleine Textdatei auf die Channel-und-Buffer-Weise, um position/limit/flip greifbar zu machen. Es öffnet die Quelle als FileChannel, liest in einen ByteBuffer, flippt, schreibt in einen Ziel-FileChannel und gibt den Buffer-Zustand bei jedem Schritt aus, damit du siehst, wie sich die Indizes bewegen.

java— editable, runs on the server

Was aus dem Lauf zu entnehmen ist:

  • Die Schleife gab den Buffer-Zustand bei jedem Schritt aus. Nach einem read() war position die Anzahl der gelesenen Bytes und limit war noch capacity — das ist der „Schreibmodus": noch Platz am Ende. Nach flip() war position = 0 und limit = die zuletzt gelesene Zahl — das ist der „Lesemodus": die Bytes liegen zwischen 0 und limit. Die zwei Indizes kodieren „wo die Daten liegen" ohne sie zu kopieren.
  • Der Buffer hatte 16 Bytes; die Datei war 44 Bytes groß. Die Schleife lief drei Iterationen: 16, 16, 12. Sobald der Buffer leer war (nachdem write ihn geleert hatte), setzte clear() ihn zurück in den „Schreibmodus", damit der nächste read() ihn neu füllen konnte. Das ist das Channel-Muster im Kleinen: füllen, flippen, leeren, bereinigen, wiederholen.
  • transferTo erledigte dieselbe Kopie in einer Zeile ohne jeglichen ByteBuffer. Unter Linux entspricht das einem einzelnen sendfile()-Syscall — die Bytes wandern Kernel-zu-Kernel, ohne die JVM zu passieren. Wenn du Daten zwischen zwei Channels verschiebst und sie dir nicht ansehen musst, ist das das richtige Werkzeug.
  • Beachte, dass die Quelldatei mit Files.writeString erstellt und das Ziel mit Files.readString zurückgelesen wurde — beides sind java.nio.file-Einzeiler, die Channels und Buffer vollständig verbergen. Die detaillierte Channel-Schleife in der Mitte wäre etwas, das du nur schreiben würdest, wenn du direkten Buffer-Zugriff benötigst (benutzerdefiniertes binäres Parsen, Memory-Mapping, Scatter/Gather). Für „eine Datei kopieren" ist transferTo oder Files.copy kürzer und mindestens genauso schnell.
  • Der FileChannel.open(path, OPTION)-Konstruktor ist das Pendant zu Files.newInputStream(path). Das StandardOpenOption-Enum (READ, WRITE, CREATE, APPEND, TRUNCATE_EXISTING, ...) steuert das Öffnungsverhalten — es gibt nur eine Stelle, an der man nachschlagen muss. Dieses Open-Options-Enum taucht im nächsten Kapitel immer wieder auf.

Was kommt als Nächstes

Dieses Kapitel hat die Bausteine benannt — Channels, Buffer, Selektoren, java.nio.file. Das nächste Kapitel, Java Path-Klasse, geht auf den benutzerfreundlichsten dieser Bausteine ein — Path — und die Methoden (resolve, relativize, normalize), die du jedes Mal verwendest, wenn du einen Dateisystempfad berührst.

Übungen

Übung
Du liest 10 Bytes von einem Channel in einen `ByteBuffer` der Kapazität 1024. Du möchtest diese 10 Bytes in einen anderen Channel schreiben. Was musst du zwischen `read()` und `write()` tun?
Du liest 10 Bytes von einem Channel in einen `ByteBuffer` der Kapazität 1024. Du möchtest diese 10 Bytes in einen anderen Channel schreiben. Was musst du zwischen `read()` und `write()` tun?
Was this page helpful?