Java: Swing und CompletableFuture

von Hubert Schmid vom 2014-06-15

Die Klasse java.util.concurrent.CompletableFuture<T> ist neu in der Standardbibliothek von Java 8, und unterstützt die asynchrone Programmierung. Die Klasse lässt sich auf vielfache Weise einsetzen. Dass sie auch für die Programmierung mit Swing – sowie anderen Frameworks mit Event-Dispatch-Thread – interessant ist, zeige ich in diesem Beitrag.

Das Swing-Framework basiert wie die meisten Frameworks für graphische Oberflächen auf einem sogenannten Event-Dispatch-Thread (EDT). Alle Ereignisse werden in diesem Threads ausgeführt, und alle Zugriffe auf Oberflächen-Komponenten dürfen lediglich aus diesem Thread erfolgen. Wichtig ist diese Eigenschaft vor allem bei blockierenden Aufrufen – gleichermaßen für Netzwerk-Operationen wie für Rechen-intensive Aufgaben. So ist beispielsweise im folgenden Listing der Aufruf der Methode fibonacci problematisch, da während der potentiell lang laufenden Berechnung die Oberfläche weder reagiert noch sich aktualisiert.

addActionListener(e -> { // executed in Event Dispatch Thread (EDT) int number = Integer.parseInt(getText()); // must be executed in EDT long result = fibonacci(number); // computation blocks EDT label.setText("> " + result); });

Um dieses Problem zu vermeiden, kann man die Berechnung in einen anderen Thread verlagern. Sobald das Ergebnis feststeht, wird eine Aktion im Event-Dispatch-Thread gestartet, um die Oberflächen-Komponenten zu aktualisieren.

Das folgende Listing zeigt eine entsprechende Implementierung. Die Lambda-Ausdrücke ermöglichen zwar eine kompakte Schreibweise im Vergleich zu anonymen inneren Klassen. Doch die strukturelle Komplexität ist unübersehbar.

addActionListener(e -> { // executed in Event Dispatch Thread (EDT) int number = Integer.parseInt(getText()); // must be executed in EDT new Thread(() -> { // asynchronous computation long result = fibonacci(number); EventQueue.invokeLater(() -> label.setText("> " + result)); // must be executed in EDT }).start(); });

Genau an dieser Stelle kommt CompletableFuture<T> ins Spiel. Die Klasse ermöglicht die Verknüpfung mehrerer Aktionen einschließlich Datenfluss. Dabei kann für jede Aktion festgelegt werden, in welchem Kontext sie auszuführen ist.

Im folgenden Listing wird zunächst die fibonacci-Methode asynchron ausgeführt. Das Ergebnis wird der zweiten Aktion übergeben, die wieder im Event-Dispatch-Thread ausgeführt wird. Im Vergleich zu obiger Implementierung vereinfacht sich dabei sowohl die Struktur als auch der Datenfluss.

addActionListener(e -> { // executed in Event Dispatch Thread (EDT) int number = Integer.parseInt(getText()); // must be executed in EDT CompletableFuture .supplyAsync(() -> fibonacci(number)) // asynchronous computation .thenAcceptAsync(n -> label.setText("> " + n), edt); // jump to EDT });

Die Klasse CompletableFuture<T> verwendet Instanzen der Schnittstelle java.util.concurrent.Executor als Ausführungskontexte. Eigentlich ist diese Schnittstelle als Abstraktion für Thread-Pools gedacht. Doch auch der Event-Dispatch-Thread lässt sich in einen Executor verpacken. Mit Hilfe einer Methoden-Referenz reicht dafür die folgende Zuweisung aus.

Executor edt = EventQueue::invokeLater;

Fazit: Die Klasse CompletableFuture<T> bietet einfache Möglichkeiten, Aktionen in unterschiedlichen Kontexten auszuführen und über den Datenfluss miteinander zu koppeln. Auch der Event-Dispatch-Thread des Swing-Frameworks kann ein solcher Kontext sein. Die Klasse macht es insbesondere möglich, blockierende Operationen sehr einfach mit Oberflächenprogrammierung zu verbinden. Eine wichtige Anforderung für korrekt und gut nutzbare Benutzeroberflächen.