1. Future: „Ich warte, bis es fertig ist“
Asynchrone Operationen in Java sind wie das Versenden eines Briefs mit der Post: Sie stehen nicht am Briefkasten und warten auf die Antwort, sondern gehen Ihren anderen Aufgaben nach. Wenn der Brief ankommt, erhalten Sie einfach eine Benachrichtigung. Im Code muss man erkennen, dass die Operation abgeschlossen ist, und das Ergebnis oder einen Fehler ermitteln. In Java gibt es zwei Ansätze: über Future und über CompletionHandler.
Mit Future erhalten Sie eine Art „Beleg“ – ein Versprechen auf ein zukünftiges Ergebnis. Sie können von Zeit zu Zeit prüfen, ob es fertig ist, oder das Ergebnis durch Aufruf von get() abwarten.
Mit CompletionHandler müssen Sie gar nicht warten: Sie geben den Handler im Voraus an, und er wird automatisch aufgerufen – bei Erfolg oder bei einem Fehler.
Wie funktioniert das?
Wenn Sie die asynchrone Methode read() oder write() bei AsynchronousFileChannel aufrufen, können Sie ein Objekt vom Typ Future<Integer> erhalten. Das ist wie eine Nummer in einer elektronischen Warteschlange: Die Operation hat begonnen, und Sie können jederzeit fragen: „Na, schon fertig?“.
Methodensignatur
Future<Integer> read(ByteBuffer dst, long position)
oder
Future<Integer> write(ByteBuffer src, long position)
- dst – der Puffer, in den Daten gelesen werden.
- src – der Puffer, aus dem Daten geschrieben werden.
- position – die Position in der Datei für Lesen/Schreiben.
Beispiel: Asynchrones Lesen mit Future
Code, der asynchron die ersten 1024 Byte aus einer Datei liest und explizit auf das Ergebnis wartet:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.io.IOException;
public class FutureReadExample {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Asynchrones Lesen starten
Future<Integer> future = channel.read(buffer, 0);
// ... hier können Sie andere Dinge tun, während die Datei gelesen wird
// Auf Abschluss der Operation warten (blockierender Aufruf!)
int bytesRead = future.get(); // Kann InterruptedException, ExecutionException werfen
System.out.println("Gelesene Bytes: " + bytesRead);
// Weiter zum Lesen aus dem Puffer
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
} catch (Exception e) {
System.err.println("Fehler beim Lesen der Datei: " + e);
}
}
}
- Die Methode future.get() blockiert den Thread bis zum Abschluss der Operation – das ist wie „am Telefon warten“, bis jemand anruft.
- Sie können future.isDone() aufrufen, um zu prüfen, ob die Operation abgeschlossen ist, und erst dann get() aufrufen.
- Wenn die Operation mit einem Fehler endet, wirft get() eine ExecutionException (mit der zugrunde liegenden Ursache im Inneren).
Wann ist Future praktisch?
Future ist praktisch, wenn Sie eine Aufgabe starten und dann zu einem passenden Zeitpunkt auf das Ergebnis warten möchten: eine Datei lesen, eine Anfrage senden, etwas im Hintergrund berechnen. Der Ansatz eignet sich auch, um mehrere Operationen parallel zu starten und anschließend die Ergebnisse einzusammeln.
Wenn Sie jedoch echte Asynchronität ohne Blockieren und mit automatischer Benachrichtigung über den Abschluss benötigen, sollten Sie CompletionHandler in Betracht ziehen.
2. CompletionHandler: „Ruf mich, wenn du fertig bist“
CompletionHandler ist ein Interface, das Sie implementieren und an die Methode read() oder write() übergeben. Wenn die Operation abgeschlossen ist (oder ein Fehler auftritt), ruft Java Ihren Handler automatisch auf. Das ist wie eine Telefonnummer zu hinterlassen: „Rufen Sie an, wenn es fertig ist.“
Methodensignatur
void read(ByteBuffer dst,
long position,
A attachment,
CompletionHandler<Integer, ? super A> handler)
- dst – Puffer zum Lesen.
- position – Position in der Datei.
- attachment – beliebiges Objekt, das dem Handler übergeben wird (kann null sein oder z. B. der Dateiname).
- handler – Ihr Handler.
Das Interface CompletionHandler
public interface CompletionHandler<V, A> {
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
- completed wird bei Erfolg aufgerufen; result – Anzahl der Bytes, attachment – Ihr Objekt.
- failed wird bei einem Fehler aufgerufen; exc – die Exception.
Beispiel: Asynchrones Lesen mit CompletionHandler
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
public class CompletionHandlerReadExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("example.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, "example.txt", new CompletionHandler<Integer, String>() {
@Override
public void completed(Integer result, String attachment) {
System.out.println("Datei " + attachment + " gelesen, Bytes: " + result);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
closeChannel();
}
@Override
public void failed(Throwable exc, String attachment) {
System.err.println("Fehler beim Lesen der Datei " + attachment + ": " + exc);
closeChannel();
}
private void closeChannel() {
try {
channel.close();
} catch (IOException e) {
System.err.println("Fehler beim Schließen des Kanals: " + e);
}
}
});
// Wichtig: Die Methode main beendet sich vor dem Ende des Lesens, wenn man den Thread nicht "festhält"!
// In realen Anwendungen endet der Thread in der Regel nicht sofort (z. B. Server, UI).
try {
Thread.sleep(500); // Wir geben der asynchronen Operation etwas Zeit (nur fürs Beispiel!)
} catch (InterruptedException ignored) {}
}
}
Wir übergeben an read() einen anonymen Handler, der weiß, was nach dem Abschluss zu tun ist. In completed erhalten wir das Ergebnis und verarbeiten es, und in failed reagieren wir auf den Fehler. Vergessen Sie nicht, den Kanal innerhalb des Handlers zu schließen, um keine Ressourcen offenzulassen.
Achtung: Die Methode main kann enden, bevor die asynchrone Operation fertig ist, daher ist im Beispiel Thread.sleep(500) hinzugefügt – nur um das Ergebnis zu sehen. In realen Anwendungen ist so ein Trick normalerweise nicht nötig.
Wann ist CompletionHandler sinnvoller?
CompletionHandler ist ideal, wenn echte Asynchronität ohne Warten und Blockieren nötig ist: Sie starten eine Operation und beschreiben, was beim Abschluss zu tun ist. Das ist für UIs (JavaFX, Swing) entscheidend, damit die Oberfläche nicht „hängt“, und auf Servern nützlich – Threads blockieren nicht im Leerlauf, sondern werden nur bei vorhandener Arbeit genutzt.
4. Vergleich von Future und CompletionHandler
| Ansatz | Blockiert den Thread? | Wann verwenden? | Beispielszenario |
|---|---|---|---|
| Future | Ja (bei get()) | Einfache sequentielle Verarbeitung | Dateikopie, Berichte |
| CompletionHandler | Nein | Truly async, UI, Server, parallele Operationen | Server, GUI, massives I/O |
- Future – leichter zu verstehen, aber für den Ergebniserhalt ist Blockieren erforderlich.
- CompletionHandler – etwas komplexer, bietet aber echte Asynchronität und blockiert Threads nicht.
5. Praxis: Asynchrones Schreiben in eine Datei mit CompletionHandler
Ein Beispiel, das eine Zeichenkette asynchron in eine Datei schreibt und über das Ergebnis informiert:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class CompletionHandlerWriteExample {
public static void main(String[] args) throws IOException {
String text = "Hallo, asynchrone Welt!";
ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
Path path = Path.of("async_output.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
@Override
public void completed(Integer result, Path attachment) {
System.out.println("Erfolgreich " + result + " Bytes in die Datei " + attachment + " geschrieben");
try {
channel.close();
} catch (IOException e) {
System.err.println("Fehler beim Schließen des Kanals: " + e);
}
}
@Override
public void failed(Throwable exc, Path attachment) {
System.err.println("Fehler beim Schreiben in die Datei " + attachment + ": " + exc);
try {
channel.close();
} catch (IOException e) {
System.err.println("Fehler beim Schließen des Kanals: " + e);
}
}
});
// Halten wir main an, um das Ergebnis zu sehen (nur fürs Beispiel)
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {}
}
}
GO TO FULL VERSION