zurück zum Artikel

Was Entwickler mit Java 8 erwartet

Michael Kofler

Nach vielen Verzögerungen ist Java 8 nun Feature-komplett und befindet sich auf der (ziemlich langen) Zielgeraden. Außer den lang erwarteten Lambda-Ausdrücken, bringt die neue Version viele Detailverbesserungen und neue Bibliotheken.

Was Entwickler mit Java 8 erwartet

Nach vielen Verzögerungen ist Java 8 nun Feature-komplett und befindet sich auf der (ziemlich langen) Zielgeraden. Voraussichtlich im März 2014 wird die neue Version mit der Unterstützung von Lambda-Ausdrücken in die Java-Geschichte eingehen. Lambda-Ausdrücke versprechen nicht nur klareren Code, sondern auch besser parallelisierbare Algorithmen. Daneben überzeugt Java 8 durch viele Detailverbesserungen und neue Bibliotheken.

Lambda-Ausdrücke sind eine kompakte Schreibweise zur Formulierung einer anonymen Klasse mit einer Methode. Solche Klassen werden häufig benötigt, um funktionale Schnittstellen zu implementieren, also Schnittstellen, die die Signatur exakt einer Methode definieren. In der Java-Klassenbibliothek gibt es eine Menge derartiger Interfaces und entsprechend viele Anwendungsmöglichkeiten für Lambda-Ausdrücke.

Der folgende Codeausschnitt zeigt, wie Lambda-Ausdrücke typischen Java-Code vereinfachen
können: Ein FilenameFilter soll alle PDF-Dateien eines Verzeichnisses ermitteln. Seine Implementierung gelingt mit einem Lambda-Ausdruck wesentlich eleganter als mit einer herkömmlichen anonymen Klasse.

// anonyme Klasse vs. Lambda-Ausdruck
import java.io.*;
...
File basedir = new File(System.getProperty("user.dir"));

// FilenameFilter als anonyme Klasse implementieren (Java 7)
FilenameFilter pf = new FilenameFilter() {
public boolean accept(File f, String s) {
return s.toLowerCase().endsWith(".pdf");
}
};

// FilenameFilter als Lambda-Ausdruck implementieren (Java 8)
FilenameFilter pf =
(f, s) -> s.toLowerCase().endsWith(".pdf");

// FilenameFilter verwenden
File[] pdfs = basedir.listFiles(pf);
for(File f : pdfs)
System.out.println(f.getName());

Die Java-Syntax für Lambda-Ausdrücke hat große Ähnlichkeit mit der von C#, wo solche Ausdrücke schon länger erlaubt sind: Einer Parameterliste in runden Klammern folgt ein Pfeiloperator und ein Ausdruck, der die Parameter verarbeitet.

Ein Lambda-Ausdruck kann auch aus mehreren Java-Kommandos bestehen. In diesem Fall ist er in geschweifte Klammern zu setzen. Wenn der Ausdruck ein Ergebnis liefert, muss es wie bei einer Methode mit return zurückgegeben werden.

// Lambda-Ausdruck ohne Parameter
() -> 7;
() -> "Ergebnis";

// Lambda-Ausdruck mit einem Parameter
(int i) -> i*i;
(i) -> i*i;
i -> i*i;

// Lambda-Ausdruck mit mehreren Parametern
(int i, String s) -> s.substring(i, i+1);
(i, s) -> s.substring(i, i+1);

// mehrteiliger Lambda-Ausdruck
(i, s) -> { kommando1;
kommando2;
return ergebnis; }

In den meisten Fällen kann der Java-Compiler aus dem Kontext den Datentyp der Parameter eines Lambda-Ausdrucks erkennen; dann ist es nicht erforderlich, eine Typbezeichnung voranzustellen. So war im ersten Codebeispiel die Variable pf als FilenameFilter definiert; damit ist für den Java-Compiler klar, dass der Lambda-Ausdruck für die Methode accept gilt, also für die einzige Methode der FilenameFilter- Schnittstelle. Die Datentypen der Parameter f und s gehen daher aus der Signatur der Methode accept hervor. So viel Compiler-Intelligenz ist übrigens kein Novum in Java: Der in Java 7 eingeführte Diamond-Operator <> zum typenlosen Aufruf des Konstruktors einer generischen Klasse agiert ähnlich.

Lambda-Ausdrücke helfen zwar bei der Implementierung anonymer Klassen, sie sind aber mehr als eine Kurzschreibweise hierfür. Für herkömmliche anonyme Klassen gelten nämlich andere Gültigkeitsebenen als für Lambda-Ausdrücke: Der Lambda-Ausdruck kann direkt auf Variablen zugreifen, die in derselben Codeebene zugänglich sind, in der er definiert wird. Der entsprechende Mechanismus wird als Variable Capture bezeichnet.

Dabei gibt es allerdings eine wesentliche Einschränkung: Lokale Variablen müssen final deklariert sein oder sich zumindest so verhalten (effectively final); das bedeutet, dass die Variablen nach ihrer ersten Zuweisung nicht mehr verändert werden und der Java Compiler eine Deklaration mit final ohne Fehlermeldung akzeptieren würde.

// Lambda-Ausdrücke können auf finale Variablen zugreifen
final String pattern = ".pdf";
FilenameFilter pf =
(File f, String s) -> s.toLowerCase().endsWith(pattern);

Auch bei this und super verhält sich Code in Lambda-Ausdrücken anders als Code in anonymen Klassen. Normalerweise bezieht sich this auf die Instanz der Klasse und super auf die Instanz der übergeordneten Klasse. Definert man also auf herkömmliche Weise eine anonyme Klasse, verweist this auf Elemente innerhalb dieser.

In Lambda-Ausdrücken haben this und super dagegen dieselbe Bedeutung wie im Code außerhalb: this bezieht sich auf Elemente der Klasse, in der der Lambda-Ausdruck definiert wird, super auf deren Basisklasse.

Es gibt unzählige Schnittstellen in der Java-Standardbibliothek, die prädestiniert zur Anwendung von Lambda-Ausdrücken sind. Das würde allerdings Änderungen an den Schnittstellen erfordern und so die Kompatibilität zu Millionen von Java-Programmen auf's Spiel setzen.

Ein Beispiel ist die Schnittstelle Iterable: Sie wurde in Java 8 um die neue Methode forEach erweitert, die einen Lambda-Ausdruck als Parameter erwartet. Eigentlich wäre jetzt der Code jeder Klasse, die von Iterable abgeleitet ist, um die Implementierung von Iterable zu erweitern. Es gäbe praktisch kein Programm, das durch das Update auf Java 8 und die neue Java-Standardbibliothek nicht grundlegend zu ändern wäre.

Der Ausweg aus diesem Dilemma sind Default-Methoden: Beginnend mit Java 8 können bei der Definition von Schnittstellen einzelne Methoden mit dem Schlüsselwort default deklariert und mit Code versehen werden. Bei der Implementierung der Schnittstelle hat der Programmierer die Wahl, entweder die Implementierung der Default-Methoden zu übernehmen oder die Methode durch eigenen Code zu überschreiben.

/// Default-Methoden in Schnittstellen
interface Iterable<T> ... {
...
void forEach(Consumer<? super T> action) default {
Iterables.forEach(this, action);
}
}

Der obige Codeausschnitt zeigt die Deklaration der neuen Methode forEach für die Iterable-Schnittstelle. Dank des Schlüsselworts default ändert sich für vorhandenen Java-6- oder Java-7-Code nichts: Sämtliche Klassen, die Iterable implementieren, funktionieren weiterhin. Aber allen Java-8-Programmierern steht nun die neue Methode forEach zur Verfügung. Sie lässt sich wahlweise in der Default-Implementierung nutzen oder durch eine eigene, für den besonderen Anwendungsfall vielleicht effizientere Variante ersetzen.

Die Schreibweise Klasse::statischeMethode übergibt eine Referenz auf eine statische Methode. Im folgenden Beispiel wird an sort die statische compare-Methode der Double-Klasse übermittelt. Alternativ lässt sich die Vergleichsmethode, also eine anonyme Comparator-Klasse, auch als Lambda-Ausdruck formulieren. (Selbstverständlich können Double-Arrays wie bisher auch ohne einen Comparator sortiert werden, weil die Double-Klasse ja ohnedies die Schnittstelle comparable implementiert; die Beispiele sollen nur die neuen Syntaxmöglichkeiten verdeutlichen.)

//  sort-Aufruf mit Methodenreferenzen
import java.util.*;
Double[] x = {1.23, 1.2, 1.29};
Arrays.sort(x, Double::compare);

// sort-Aufruf mit Lambda-Ausdruck
Arrays.sort(x,
(d1, d2) -> d1==d2 ? 0 : (d1<d2 ? -1: 1));

Auch die nicht statischen Methoden von Objekten (also von konkreten Instanzen einer Klasse) lassen sich als Parameter übergeben. Die Schreibweise lautet objektvariable::instanzMethode. In der Praxis ist diese Syntax aber selten hilfreich, weil damit zwar eine Referenz auf die Methode übergeben wird, nicht aber das zu bearbeitende Objekt.

Java unterstützt deswegen eine zweite Syntaxvariante, die formal gleich wie bei statischen Methoden aussieht: klasse::instanzMethode. Tatsächlich wird dadurch aber ein Lambda-Ausdruck der folgenden Form gebildet:

(Klasse obj) -> { obj.instanzMethode(); }

Zweckmäßig ist diese Schreibweise zum Beispiel in Kombination mit der neuen forEach-Methode diverser Aufzählungsklassen. Sie können nun an forEach eine Methode übergeben, die dann auf alle Objekte der Aufzählung angewandt wird:

lst.forEach(MyClass::printout);

Lambda-Ausdrücke eröffnen generell ganz neue Wege im Umgang mit Aufzählungen. Dabei geht es weniger darum, Code eleganter zu formulieren, vielmehr sollen in Java 8 einzelne Methoden zur Verarbeitung von Aufzählungen besser parallelisiert und erst bei Bedarf vollständig ausgewertet werden (durch so genannte Lazy Operations). Möglich wird das durch Erweiterungen in den Collection-Klassen der Java-Standardbibliothek.

forEach ist für die Schnittstellen Iterable und Iterator als Alternative zu herkömmlichen for-each- Schleifen gedacht. Die sogenannte Internal Iteration die die Methode verwendet, bietet im Vergleich zur traditionellen External Iteration zwei wesentliche Vorteile: Zum einen können Lambda-Ausdrücke komfortabel auf alle Aufzählungselemente angewendet werden, zum anderen lässt sich die forEach-Methode so implementieren, dass die Verarbeitung der Elemente parallelisiert erfolgt.

Neben forEach gibt es andere neue Methoden, die verschiedene Formen von Lambda-Ausdrücken als Parameter erwarten. Zu deren Deklaration sieht das neue Paket java.util.functions diverse Schnittstellen vor (siehe Tabelle 'Lambda-Schnittstellen'). Sie kommen zum Beispiel in den Parametern verschiedener Methoden der Collection-Schnittstellen zum Einsatz:

Predicate<T> überprüft, ob ein Objekt vom Typ T ein Kriterium erfüllt.
Supplier<T> liefert Objekte vom Typ T (z.B. für get-Methoden).
Consumer<T> verarbeitet ein Objekt vom Typ T, gibt kein Ergebnis zurück.
Consumer<T, U> verarbeitet zwei Objekte vom Typ T und U, gibt kein Ergebnis zurück.
Function<T, R> verarbeitet ein Objekt vom Typ T und liefert als Ergebnis ein Objekt vom Typ R zurück.
BiFunction<T, U, R> verarbeitet zwei Objekte vom Typ T und U und liefert als Ergebnis ein Objekt vom Typ R zurück.
UnaryOperator<T> entspricht Function<T, T>, d.h., die zu verarbeitenden Daten und die Ergebnisse weisen denselben Typ auf.
BinaryOperator<T> entspricht BiFunction<T, T, T>, d.h., die zu verarbeitenden Daten und die Ergebnisse weisen denselben Typ auf.

Das folgende Beispiel zeigt den Einsatz der neuen Methode removeIf. Sie entfernt alle Elemente einer Aufzählung, die ein bestimmtes Kriterium erfüllen. Hierfür erwartet removeIf ein Predicate-Objekt, das sich am einfachsten als Lambda-Ausdruck formulieren lässt.

import java.util.*;

List<Integer> lst = new ArrayList<>();
for(int i=1; i<10; i++)
lst.add(i);
// alle ungeraden Zahlen entfernen
lst.removeIf(i -> i % 2 == 1);

Die spannendste Neuerung ist aber im Paket java.util.stream versteckt. Die dort enthaltenen Schnittstelle Stream<T> erweitert die grundlegenden Aufzählungsklassen um ein vollkommen neues Konzept zur funktionalen Programmierung. Die Elemente werden dabei nicht gespeichert, sondern von einer Methode an die nächste weitergereicht (von der Idee her ähnlich wie Pipes unter Unix/Linux) und erst bei Bedarf tatsächlich verarbeitet (Lazy Operation). Wichtige Methoden sind filter, map, reduce, fold, limit und skip.

import java.util.*;

String lorem = "Lorem ipsum dolor sit amet, ...";
List<String> lst = Arrays.asList(lorem.split(" "));

// alle Wörter mit mehr als sechs Zeichen ausgeben
// Ausgabe: consetetur sadipscing invidunt aliquyam
// voluptua. accusam dolores gubergren, ...
lst.stream()
.filter(s -> s.length()>6)
.forEach(s -> System.out.println(s));

// alle Wörter mit mehr als sechs Zeichen zählen (ohne Doppelgänger)
long n = lst.parallelStream()
.filter(s -> s.length()>6)
.distinct()
.count();
System.out.println(n);

// durchschnittliche Wortlänge
OptionalDouble avg = lst.parallelStream()
.mapToInt(s -> s.length())
.average();
System.out.println(avg.getAsDouble());

Der obige Code zeigt, wie eine Liste von Zeichenketten in einen Stream umgewandelt und dann auf unterschiedliche Weise verarbeitet wird. Bemerkenswert ist die Methode parallelStream: Sie liefert einen zur Parallelverarbeitung optimierten Stream.

Der verunglückten Date-Klasse in der Java-Standardbibliothek wurde bereits in Java-Version 1.1 die Calendar-Klasse zur Seite gestellt, doch auch sie ist eher eine Notlösung. Als gegenwärtig beste Möglichkeit, diffizile Datumsfragen zu behandeln, gilt die unter der Apache-2-Lizenz verfügbare Bibliothek Joda Time.

In Java 8 wagt Oracle mit dem sogenannten ThreeTen-Projekt [1] nun einen weiteren Anlauf, um Daten, Zeiten und ihre unzähligen Sonderfälle innerhalb der Standardbibliothek in den Griff zu bekommen – und versucht gleichzeitig, Designschwächen der Joda-Time-Bibliothek zu beheben [2]. Damit ist schon klar: Java-Entwickler müssen sich auf eine weitere Bibliothek einlassen, die inkompatibel zu allen bisherigen Varianten und mit über 60 Klassen und Schnittstellen in mehreren java.time-Paketen alles andere als übersichtlich [3] ist.

Eine wichtige neue Idee des Projekts ist die Unterscheidung zwischen einer Machine Time Line und einer Human Time Line:

// Anwendung der Zeit- und Datum-Klassen
import java.time.*;
import java.util.*;
import java.time.format.*;

LocalDateTime dt = LocalDateTime.now();
System.out.format("Jahr: %d\n", dt.getYear());
System.out.format("Monat: %d\n", dt.getMonthValue()); // 1-12
System.out.format("Formatiert: %s\n", // z.B. 2013-06-06T13:29:50.252
dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

// der Monatsname
Month m = dt.getMonth();
String s = m.getDisplayName(TextStyle.FULL, new Locale("de"));
System.out.format("Monat: %s\n", s); // z.B. Juni

// das Datum in einem Monat
LocalDateTime dt2 = dt.plusMonths(1);
System.out.format("Formatiert: %s\n", // z.B. 2013-07-06
dt2.format(DateTimeFormatter.ISO_LOCAL_DATE));

Standardmäßig verwenden die ThreeTen-Klassen den in Europa üblichen ISO-Kalender. Zum Arbeiten in anderen Kalendersystemen stellen die Pakete java.time.chrono und java.time.calendars einige Zusatzklassen zur Verfügung (z.B. MinguoDate und MinguoChronology für den in Taiwan geltenden Minguo-Kalender).

Java 8 erweitert die in Java 5 eingeführten Annotationen: Bisher konnten Annotationen nur für Klassen, Methoden, Felder und Variablen verwendet werden. Ab Java 8 lassen sich auch alle anderen Java-Typen durch Metainformationen ergänzen, zum Beispiel generische Typen, Arrays, Casting-Operatoren oder throws-Ausdrücke.

// Annotationen für generische Typen
Map<@NonNull String, @NonEmpty List<String>> mymap;

Rund um Annotationen gibt es noch mehr Änderungen: Eine Annotation lässt sich nun mehrfach auf ein Element anwenden (zum Beispiel @Author("name1") @Author("name2")). Außerdem wurde die Liste der vordefinierten Annotationen um @FunctionalInterface erweitert: sie ist zur Kennzeichnung funktionaler Schnittstellen gedacht. Dabei handelt es sich um solche mit nur einer abstrakten Methode. Wenn diese Annotation verwendet wird, überprüft der Compiler, ob die Regeln für funktionale Schnittstellen erfüllt sind.

Mit Java 8 wird die in Enterprise- bzw. Server-Anwendungen mitunter eingesetzte JavaScript-Engine Rhino durch die vollkommen neue Implementierung Nashorn ersetzt. Nashorn soll JavaScript-Code wesentlich schneller ausführen und gleichzeitig sparsamer mit dem Speicherplatz umgehen.

Das Framework JavaFX wird mit Java 8 in das JDK integriert, was den Versionssprung von JavaFX 2.2 auf JavaFX 8 begründet. Auch inhaltlich hat sich einiges getan: zu den wichtigsten Neuerungen zählen 3D-Funktionen, Klassen zur Auswertung von Sensoren, wie sie auf Smartphones häufig zu finden sind, bessere Möglichkeiten zur Textformatierung und zum Ausdruck formatierter Dokumente, das neue TreeTableView-Steuerelement sowie eine verbesserte HTML5-Unterstützung.

In die Rubrik "nützliche Kleinigkeiten" lässt sich die neue Methode parallelSort der Arrays-Klasse einordnen. Sie ist symptomatisch für diverse andere Detailverbesserungen, die Oracle an der Java-Standardbibliothek vorgenommen hat – etwa die Unterstützung von Unicode 6.2, die Implementierung besserer Verschlüsselungsalgorithmen und sicherere Zufallszahlen etc. Jede Verbesserung ist für sich kaum der Rede wert, in Summe machen sie die Arbeit mit Java aber produktiver und die resultierenden Programme – zumindest in manchen Fällen – schneller.

Mit der Fertigstellung des Milestone 7 Ende Mai 2013 ist Java 8 Feature-komplett. Der weitere Zeitplan sieht im September 2013 eine Developer Preview, im Oktober den API/Interface Freeze und im Januar 2014 einen Final Release Candidate vor. Wirklich fertig soll Java 8 dann im März 2014 sein. Die vielfachen Verzögerungen haben nicht zuletzt damit zu tun, dass große Teile des Java-Entwickler-Teams seit Monaten damit beschäftigt sind, immer neue Sicherheitsprobleme in älteren Java-Versionen zu beheben.

Entwickler können den Großteil der neuen Features schon jetzt ausprobieren und wöchentlich neue Early-Access-Testversionen [4] (Builds) von der JDK8-Website [5] herunterladen. Um Konflikte mit vorhandenen Java-Installationen aus dem Weg zu gehen, empfiehlt es sich, die Installation in einer virtuellen Maschine durchzuführen.

java -version 
java version "1.8.0-ea"
Java(TM) SE Runtime Environment (build 1.8.0-ea-b92)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b34, mixed mode)

Auf den Komfort von Eclipse müssen Entwickler bei ihren Tests allerdings verzichten, denn auch die aktuelle Eclipse-Version Kepler ist nicht Java-8-kompatibel. Das liegt daran, dass Eclipse nicht auf den mit dem JDK mitgelieferten Compiler javac zurückgreift, sondern einen eigenen verwendet – und der versteht die Lambda-Syntax noch nicht. NetBeans-Fans haben es besser – für diese IDE existiert bereits eine recht brauchbare Lamba-kompatible Testversion [6]. Und nicht nur das: Der NetBeans-Editor hilft sogar dabei, anonyme Klassen auf Knopfdruck in Lambda-Ausdrücke umzuwandeln.

Es gibt bereits eine Lambda-kompatible Testversion von NetBeans.

Es gibt bereits eine Lambda-kompatible Testversion von NetBeans.


Seit Jahren reden Java-Entwickler über die bessere Modularisierung von Java-Programmen und -Bibliotheken. Ein Ziel des der Diskussion zugrundeliegenden Projekts Jigsaw ist es, Java-Programme mit einer minimalen Runtime-Umgebung ausliefern zu können, die wirklich nur die Klassen enthält, die vom Programm tatsächlich genutzt werden.

Obwohl das Projekt Jigsaw weit fortgeschritten ist und ursprünglich als wesentliches Feature von Java 7 (!) gedacht war, musste seine Fertigstellung nun neuerlich verschoben werden – auf die Java-Version 9, für die es noch nicht einmal einen konkreten Zeitplan gibt.

Daneben hat Oracle auf der JavaOne 2012 recht vage einige weitere Ziele für Java 9 formuliert, unter anderem die Fertigstellung einer selbst-optimierenden Java Virtual Machine, die Unterstützung von Tail Calls (also den effizienten Aufruf einer Methode am Ende einer anderen Methode) sowie die bessere Nutzung von Grafikkarten zum Durchführen aufwendiger Berechnungen (OpenJDK-Projekt Sumatra). Wie die Erfahrungen mit Java 7 und 8 gezeigt haben, hat diese Zielvorgabe einen ähnlichen Wert wie ein Brief an das Christkind; welche Features Java 9 tatsächlich bieten wird, werden wir erst in einigen Jahren wissen.

Java 8 ist wie jedes andere Software-Produkt von Kompromissen geprägt. Viele Java-Entwickler hätten sich in Java 8 noch mehr Features gewünscht. Doch auch mit den verbliebenen Neuerungen kann man Java 8 durchaus als revolutionäre Version bezeichnen, die die Programmierung in Java mindestens ebenso stark verändern wird, wie die in Java 5 eingeführten Generics. Lambda-Ausdrücke und ihre Einbettung an verschiedenen Orten in der Standardbibliothek eröffnen vollkommen neue Wege, um Algorithmen mit einfachen Mitteln besser zu parallelisieren.

Michael Kofler [7]
ist freier Computerbuchautor und Lehrbeauftrager an der Fachhochschule Kapfenberg. Er hat kürzlich ein E-Book zur Java-Syntax veröffentlicht.


Literatur:

(jul [11])


URL dieses Artikels:
https://www.heise.de/-1932997

Links in diesem Artikel:
[1] http://download.java.net/jdk8/docs/api/java/time/
[2] http://blog.joda.org/2009/11/why-jsr-310-isn-joda-time_4941.html
[3] http://sourceforge.net/apps/mediawiki/threeten/?title=User_Guide
[4] http://jdk8.java.net/download.html
[5] http://jdk8.java.net
[6] http://bertram2.netbeans.org:8080/job/jdk8lambda/lastSuccessfulBuild/artifact/nbbuild/
[7] http://kofler.info/
[8] http://download.java.net/jdk8/docs/index.html
[9] http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-4.html
[10] http://datumedge.blogspot.co.at/2012/06/java-8-lambdas.html
[11] mailto:jul@heise.de