Mitte März veröffentlichte Oracle die achte Version von Java. Neben kleinen Detailverbesserungen erweitert die lange erwartete Release die Kernsprache um Elemente der funktionalen Programmierung – die erste wesentliche Weiterentwicklung seit Java 5.
Der langjährige Chef der Java-Entwicklung, Mark Reinhold, konnte im März Version 8 von Java freigeben. Nach zweieinhalb Jahren Arbeit steht die Weiterentwicklung der Sprache nun zur Verfügung. Sie enthält nicht nur kleinere Erweiterungen der Laufzeitbibliothek, mit den Lambdas ziehen auch funktionale Elemente in Java ein.
Aufgeschoben
Dabei verlief die Entwicklung der neuen Version nicht wesentlich glücklicher als die der vorigen. Zur Erinnerung: Im August 2010 zog Reinhold die Reißleine und reduzierte den geplanten Funktionsumfang von Java 7, damit es im Juli 2011 fertig werden konnte. Die verschobenen Features sollte die Version 8 Ende des Jahres 2012 liefern, woraus schließlich Anfang 2014 und ein wiederum reduzierter Funktionsumfang wurden.
Grund für die Verzögerung war vor allem die dringend notwendige Behebung der Sicherheitsmängel in Applets und Java Webstart [1]. Eigentlich waren Sprache und Laufzeitumgebung von Anfang an so geplant, dass bösartiger Code nicht aus der zugewiesenen Sandbox ausbrechen kann. Die Umsetzung der Konzepte wies aber doch wesentliche Schwachstellen auf, sodass mancher Windows-Nutzer unliebsame Bekanntschaft mit dem über ein Java-Applet installierten BKA-Trojaner machte [2].
Den Arbeiten zur Schließung der Sicherheitslücken fiel vor allem die geplante Modularisierung der Java-Laufzeitumgebung zum Opfer (Projekt Jigsaw, [3]). Dank der ARM-Portierung läuft Java inzwischen zwar problemlos auf dem Raspberry Pi, aber kaum eine Anwendung auf diesem kleinen Rechner dürfte die gesamte Java-Standardbibliothek samt Corba-Stack und LDAP-Unterstützung benötigen. Mit Jigsaw könnten die Entwickler die Laufzeitumgebung auf den wirklich benötigten Umfang beschränken und damit den Installationsumfang und die Startzeit verringern.
Lambdas
Auch Programmiersprachen unterliegen Moden, und derzeit gewinnt die funktionale Programmierung immer mehr Anhänger. Anders als bei objektorientierten Sprachen stehen hier die Funktionen im Vordergrund. Sie lassen sich nicht nur im Kontext einer Klasse definieren und ausführen, sondern stellen eine eigenständige Entität dar. Der Programmierer kann sie weiterreichen und mit anderen Funktionen verketten.
Martin Odersky von der ETH Lausanne zeigt mit seiner Sprache Scala [5] seit einigen Jahren, wie die Kombination von funktionalen und objektorientierten Features in einer Sprache aussehen kann. Mit Java 8 wandern diese Ideen direkt in die Wirtssprache Java. In Verbindung mit einer knappen Syntax sind damit viele Aufgaben auch in Java viel kompakter programmierbar als bisher.
Java wurde von Grund auf als objektorientierte Sprache entworfen, daher lassen sich Methoden nicht einfach zu vollwertigen Mitgliedern der Sprache neben den Objekten befördern. Im Hintergrund kommen deshalb minimale Objekte mit nur einer Methode zum Einsatz, die sich mit der Lambda-Syntax verkürzt schreiben lassen.
Basis für die Umsetzung ist das »FunctionalInterface« , eine Annotation für ein Interface mit einer Methode. Es stellt sozusagen die Brücke zwischen der Objekt- und der Funktionswelt dar. Java 8 wendet es zum Beispiel auf das altbekannte Comparator-Interface an (Listing 1). In den bisherigen Java-Versionen ist zum Implementieren des Interface mindestens eine anonyme Klasse erforderlich (Listing 2, ab Zeile 3), im schlimmsten Fall definiert der Programmierer den Comparator in einer eigenen Klasse.
Listing 1
FunctionalInterface und Default Implementation
01 package java.util;
02
03 // Java 8: Definition als FunctionalInterface
04 @FunctionalInterface
05 public interface Comparator<T> {
06
07 // Dies ist DIE Methode im FunctionInterface
08 int compare(T o1, T o2);
09
10 // Java 8 : statische Methoden mit Implementation im Interface
11 public static <T, U> Comparator<T> comparing(
12 Function<? super T, ? extends U> keyExtractor,
13 Comparator<? super U> keyComparator) {
14 [...]
15 }
16 // Java 8: Default Implementation im Interface
17 default Comparator<T> reversed() {
18 return Collections.reverseOrder(this);
19 }
20 }
Listing 2
Lambdas in Java 8
01 List<Rectangle> vierecke = [...]
02
03 // Java 1-7: Anonyme, innere Klasse
04 Comparator<Rectangle> compr7 = new Comparator<Rectangle>() {
05 @Override
06 public int compare(Rectangle o1, Rectangle o2) {
07 return o1.width - o2.width;
08 }
09 };
10 vierecke.sort(compr7);
11
12 // Java 8: Kurzdefinition als Lambda
13 vierecke.sort( (Rectangle r1, Rectangle r2) -> r1.width - r2.width);
14 vierecke.sort( (r1, r2) -> r1.width - r2.width);
15
16 // Java 8: Nutzung von statischen Methoden und -referenzen
17 vierecke.sort(Comparator.comparingDouble(Rectangle::getWidth));
18 // Java 8: Nutzung von Default-Implementation aus Interface
19 vierecke.sort(Comparator.comparingDouble(Rectangle::getWidth).reversed().thenComparing(
20 Comparator.comparingDouble(Rectangle::getHeight)));
Das ist verhältnismäßig viel Schreibarbeit, denn die eigentliche Berechnung besteht in dem Beispiel nur aus dem Code »o1.width – o2.width« . Mit der Lambda-Schreibweise in Zeile 13 wird das ganze wesentlich kompakter. Der Einzeiler
ints.sort((Rectangle r1, Rectangle r2)-> r1.width - r2.width );
definiert eine Comparator-Klasse inklusive Sortierfunktion und wendet sie auf die Liste an.
Das Definieren einer Lambda-Funktion erfolgt in drei Teilen: erst die Definition der Eingangsparameter »(Rectangle r1, Rectangle r2)« , dann der so genannte Burger Arrow »->« und schließlich die eigentliche Methodenimplementierung »r1.width – r2.width« . Ein Methodenname ist nicht notwendig, durch die Definition der Methode »Collection.sort« ist dem Compiler bereits bekannt, welches »FunctionalInterface« dazu passt, welches wiederum nur eine Methode enthalten kann.
Im Endeffekt kann Java auch die Typdefinition der Eingabeparameter aus dem Interface übernehmen, sodass sich das Ganze wie in Zeile 14 noch kompakter schreiben lässt. Auf diese Weise bleiben von den ursprünglich 155 Zeichen gerade mal 42 übrig.
Fertiggericht
Wie man am altbekannten »java.util.Comparator« sieht, wurde »FunctionalInterface« überall in der Standardbibliothek nachgepflegt. Neben den Lambdas hat sich die Syntax mit Java 8 noch um zwei weitere Merkmale erweitert: statische Methodendefinition im Interface und Default-Implementierung.
Ein Beispiel für die statische Methodendefinition zeigt Zeile 17 in Listing 2. Die Lambda-Definition des Comparators aus Zeile 14 ist zwar schön knapp, doch wie man zwei Zahlen miteinander vergleicht, ist hinreichend bekannt und muss nicht neu erfunden werden.
Eine fertige Implementierung für solche Trivialaufgaben lässt sich in Java 8 als statische Methode im Interface definieren. Der Entwickler braucht ihr nur noch zu sagen, wie sie an die zu vergleichenden Zahlen kommt. Dies wiederum geschieht als Lambda-Funktion, denn mit der neuen Syntax lassen sich auch Methoden-Referenzen definieren.
Die Schreibweise »Rectangle::getWidth« in Listing 2 verweist auf die Methode »getWidth« aus der Klasse »Rectangle« . Der Code wendet sie auf die übergebenen Rectangle-Objekte an, um die Double-Werte zu erhalten. Zur deren Sortierung kommten die Regeln aus der statischen Interface-Methode »comparingDouble« von Comparator zur Einsatz.
Verkettet
Wie Zeile 19 zeigt, sind mit der neuen Schreibweise auch komplexe Aufgaben leicht zu erledigen, denn einzelne statische Methoden lassen sich verketten. Im Beispiel sortiert die Methode »comparingDouble()« zunächst die Rechtecke aufsteigend nach ihrer Breite. Die Default-Methode (auch Defender-Methode) »reversed()« kehrt die Reihenfolge danach um und übergibt das Ergebnis mit »thenComparing()« an den nächsten Comparator. Dieser ist wieder ein Lambda-Ausdruck und sortiert die Rechtecke aufsteigend anhand ihrer Höhe.
Was hier nur bequem erscheint, erlaubt die elegante Definition erweiterbarer Interfaces. Alle Klassen, die das Comparator-Interface implementieren, nutzen erst einmal die Default-Implementierung der Methode. Benötigt der Programmierer anderes Verhalten, kann er es in der Klasse überladen. Damit gelingt es, das Verhalten von Objekten bereits weitgehend auf Interface-Ebene zu definieren, was im Vergleich zum bisher benötigten Dreiklang aus Interface, abstrakter Klasse und Klasse bei vergleichbaren Aufgaben wieder zu kompakterem Quellcode führt.
Verstromt
Das Map-Reduce-Schema hat sich als ein Standard-Pattern für Daten-Konvertierung und -Extraktion etabliert. Mit den Streams findet es erstmals seinen Weg in die Java-Standardbibliothek. Ein Stream entsteht immer auf Basis vieler Objekte, etwa aus einer Liste. Auf die Objekte des Streams wendet man dann Zwischenoperationen wie Konvertieren oder Filtern an, und schließlich endet er in einer finalen Operation, die das Ergebnis aufsammelt oder auf einen Endwert reduziert.
Ein erstes Beispiel enthält Zeile 3 in Listing 3, die die Fläche aller Rechtecke mit einer Mindestgröße summiert. Dazu verwandelt der Code die Liste der Rechtecke mit der neuen Methode »stream()« in einen Stream und wendet auf diesen nacheinander Methoden zum Filtern (»filter()« ), Umwandeln (»map()« ) und Reduzieren (»sum()« ) an. Das ließe sich natürlich auch mit einer Schleife und diversen Zwischenschritten implementieren, doch mit Lambdas und den vordefinierten Methoden fällt der Schreibaufwand viel geringer aus.
Listing 3
Streams in Java 8
01 List<Rectangle> vierecke = [...] 02 // Fläche aller Viereecke mit einer Länge und Breite größer 2 03 int fläche = vierecke.stream().filter( (Rectangle v) -> v.width > 2 && v.height > 2). // 04 mapToInt( (Rectangle v) -> v.width * v.height).sum(); 05 06 // Parallele Konvertierung von Rectangle nach Rectangle2D.Double 07 Set<Rectangle2D.Double> vierecke2D = vierecke.stream().parallel(). // 08 map(v -> new Rectangle2D.Double(v.x, v.y, v.width, v.height)).// 09 collect(Collectors.toSet() );
Ein großer Vorteil von Streams gegenüber einer einfachen Schleife versteckt sich im zweiten Beispiel ab Zeile 6. Im Normalfall arbeitet Java Streams sequenziell ab, die Methode »parallel()« fordert jedoch einen parallel arbeitenden Stream an. Dabei führt Java die Operationen automatisch über mehrere Threads verteilt und parallel aus. Auf einem heutigen Multicore-Prozessor geht das natürlich viel schneller als die sequenzielle Bearbeitung. Im Vergleich zur bisher notwendige Umsetzung mit einem »ThreadPool« und Futures ist auch der Schreibaufwand wesentlich geringer.
Im Beispiel konvertiert die Methode »map()« nur die Rechtecke der Eingangsliste in Objekte der Klasse »Rectangle2D.Double« , die Methode »collect()« fasst diese in einem Set zusammen. Zum Aufsammeln nutzt der Code die neue Klasse »Collectors« . Sie unterstützt neben trivialen Zielen wie Sets oder Listen auch komplexere Dinge wie fertig gruppierte Objekte in Maps.
Tempus fugit
Kaum ein anderer Bereich der Java-Standardbibliothek war bisher so stiefmütterlich umgesetzt wie die Zeitbehandlung. Die Klasse »java.util.Date« stellt nur eine dürftige Hülle um die seit dem 1.1.1970 verstrichenen Millisekunden dar und kennt keine Zeitzonen. Auch die mit Java 1.1 eingeführte Klasse »java.util.Calendar« weckt keine Begeisterung, weshalb immer mehr Entwickler die Joda-Time-Bibliothek [4] nutzen.
Doch auch Joda-Time hat noch ein paar Design-Macken, deshalb definieren die Entwickler unter Leitung des Joda-Time-Autors Steven Colebournes ein komplett neues API für die Zeitbehandlung (JSR 310). Es wird mit Java 8 Teil der normalen Laufzeitbibliothek, sodass sich nun auch aufwändige Zeitberechnungen und -formatierungen ohne zusätzliche Bibliothek implementieren lassen.
Das Interface ist auch sicherer geworden, Zeitpunkte sind nun »immutable« , und für Wochentage und Monate kommen Konstanten zum Einsatz. Listing 4 gibt als Beispiel einen Zeitstempel für den Vorabend der Umstellung auf mitteleuropäische Sommerzeit aus (Abbildung 1), danach die korrekte Zeitangabe 25 Stunden später (30. März 2014 22:10:05 MESZ).
Listing 4
Neues API zur Zeitbehandlung
01 DateTimeFormatter format8 =
02 DateTimeFormatter.ofPattern("EEEE, dd. MMMM yyyy HH:mm:ss z");
03
04 ZonedDateTime zs8 = LocalDateTime.of(2014, 3, 29, 20, 10, 5).atZone(ZoneId.of("Europe/Paris"));
05 System.out.format("vor der Umstellung \t: %s\n", zs8.format(format8));
06
07 Duration dauer = Duration.ofHours(25);
08
09 ZonedDateTime später8 = zs8.plus(dauer);
10 System.out.format("25 Stunden später \t: %s\n", später8.format(format8) );
Gäste
Zunehmend nutzen andere Programmiersprachen die Java-Laufzeitumgebung als Unterbau. Das begann mit Portierungen alter Bekannter wie Python oder Tcl, gilt aber auch für komplett neue Sprachen wie Scala [5], Ceylon [6], oder Groovy [7]. Damit diese Sprachen möglichst schnell auf der JVM laufen, bekam Java 7 die Unterstützung für dynamische Sprachen eingebaut (»invokedynamics« ). Einer der Nutznießer ist der von Oracle entwickelte Javascript-Interpreter Nashorn. Er ersetzt mit Java 8 den alten Rhino-Interpreter und bietet neben 100-prozentiger Unterstützung der Javascript-Spezifikation [8] (Rhino: 95 Prozent, Firefox 26: 99,3 Prozent) auch eine wesentlich bessere Performance.
In Tests gegen den Benchmark Sunspider [9] schlägt sich Nashorn im Mittel um Faktor vier besser als Rhino, einzelne Tests laufen bis zu 40-mal schneller ab (Abbildung 2). Damit sind nicht nur die Versprechungen von »invokedynamics« eingelöst, es steht mit Javascript auch eine leichtgewichtige Skriptsprache für Java-Anwendungen bereit, die sich ohne zusätzliche Bibliotheken nutzen lässt.
Special Effects
Eine interessante Entwicklung hat die Rich-Client-Bibliothek Java FX hinter sich (Abbildung 3). Einst mit viel Vorschusslorbeeren als designierter Nachfolger von Swing gestartet, wurde sie nach einem Fehlstart schon totgesagt. Sie tauchte in den letzten zwei Jahren kaum in der Presse auf, erfuhr unterdessen mit Java FX 2.0 aber den Umbau in eine normale Java-Bibliothek. Sie ermöglicht wie etwa SVG die Entwicklung von Oberflächen auf Basis eines Scene Graph, der neben Grafik-Primitiven wie Linien und Flächen auch Widgets wie »TreeTable« oder Kalender enthalten kann. Mit Java 8 kommt FX nun erstmals in größerem Maßstab unters Volk: Oracle bietet es für Linux, Apple und Windows an. Leider betreibt Oracle die Portierung für Android und I-OS nicht selber, und Community-Projekte wie [10] erhalten nicht den manchmal notwendigen Unternehmensstempel. Dabei wäre die Unterstützung mobiler Geräte und Desktops mit einer einzigen GUI-Bibliothek ein Killer-Feature.
Desktop und mobil
Von Ausnahmen wie Minecraft abgesehen sind neue Java-Desktop-Anwendungen nicht der Regelfall, meist fällt die Wahl derzeit auf HTML 5. Damit ersparen sich Hersteller zwar die Desktop-Abteilung, schränken sich technologisch aber deutlich ein. Während Java-FX-Anwendungen auf alle Java-Bibliotheken sowie auf lokale Dateien zugreifen dürfen, geht dies bei Webanwendungen zu Recht nur stark eingeschränkt. Galt mancher vor einem Jahr noch als Sicherheitsfanatiker, wenn er die Nutzung lokaler Schlüssel und Zertifikate als Notwendigkeit betrachtete, hat sich dies inzwischen als Standardanforderung bei Unternehmensanwendungen etabliert und erfordert nach wie vor einen Rich Client.
Einen anderen Anwendungsfall präsentierten Jasper Pott und Richard Blair bei der letzten Java One im September 2013. Das Duke Pad [11] ist ein Tablet auf Basis des Raspberry Pi, das Linux als Betriebssystem und Java FX für die Oberfläche verwendet. Die Pläne für die Hardware stehen unter GPLv2 zur Verfügung, was in Verbindung mit dem offenen Linux, Java und Java FX zu einem kompletten Open-Source-Gerät führt. Es wird vermutlich nicht die üblichen Android-Tablets in der Publikumsgunst überholen. Als Basis für Kiosk-Anwendungen oder Displays in der Maschinensteuerung aber bietet es eine attraktive Entwicklungsumgebung.
Alte Zöpfe
Java rangiert seit Jahren neben C-basierten Sprachen (C, C++ und Objective-C) auf den oberen drei Plätzen der Programmiersprachen-Hitparaden. Wegen der schleppenden Weiterentwicklung hat es sich leider zunehmend einen Ruf als neues Cobol eingefangen. Mit Java 8 kommt die lang ersehnte Bewegung in die Sprache. Viele Programmieraufgaben lassen sich nun wesentlich kompakter lösen, ohne gleich Perl-ähnlich in die Unleserlichkeit abzugleiten.
Ebenso positiv zu bewerten ist das Verhältnis von Java zur Community. Oracle hat sich an sein mit Java 7 gegebenes Versprechen gehalten, die Neuentwicklung offener gestaltet und den Code unter Open-Source-Lizenz veröffentlicht. Die Quellen für Java 8 mit allem, was dazugehört, stehen unter [12] zur Verfügung, das Gleiche gilt schon jetzt für Java 9. Die Wunschliste für Java 9 steht bereits – ganz oben die Veröffentlichung zum geplanten Termin 2015. Bei der Modularisierung von Java (Projekt Jigsaw) ist politisch und technisch zu klären, wie die Überschneidung mit dem Modulsystem OSGI [13] zu lösen ist. Weniger Zündstoff ist beim Projekt Sumatra [14] zu befürchten, hier geht es um die Nutzung von GPUs für Java-Anwendungen.
Jenseits von Java 9 ist der Ruf zum Aufräumen nicht mehr zu überhören. Bisher achtet Java auf strikte Rückwärtskompatibilität, doch langsam ist es Zeit, die überholten alten Klasse und Methoden zu entfernen. Inzwischen ist die Liste in der Javadoc auf 14 Seiten angeschwollen, einige davon sind schon seit Java 1.1 als ungültig markiert. Hier sollte Oracle über seinen Schatten springen und sie nach 17 Jahren endlich entfernen. (mhu)
Infos
- “Secure the train”: http://mreinhold.org/blog/secure-the-train
- Sicherheitsloch in »Applet« : http://seclists.org/fulldisclosure/2013/Jan/241
- Projekt Jigsaw: http://openjdk.java.net/projects/jigsaw/
- Joda-Time: http://www.joda.org/joda-time/
- Scala: http://www.scala-lang.org
- Ceylon: http://www.ceylon-lang.org
- Groovy: http://groovy.codehaus.org
- ECMA-Script Language test262: http://test262.ecmascript.org
- Sunspider-Benchmark: http://www.webkit.org/perf/sunspider/sunspider.html
- Java FX für Android: https://bitbucket.org/johanvos/jfx78
- Duke Pad: https://wiki.openjdk.java.net/display/OpenJFX/DukePad
- Open JDK: http://openjdk.java.net
- OSGI: http://www.osgi.org/Main/HomePage
- Sumatra: http://openjdk.java.net/projects/sumatra/
- Listings zum Artikel: https://www.linux-magazin.de/static/listings/magazin/2014/05/java









