Bereits seit dem 26. Mai 2016 befindet sich Java 9 im „Feature Complete“-Stadium [1]. Somit ist es bereits jetzt möglich, sich einen Build des Early Access Release herunterzuladen [2] und neue Features auszuprobieren.
Da die vier großen Änderungen Jigsaw [3], JShell [4], HTTP/2 [5] und das Money and Currency API [6] bereits Gegenstand zahlreicher Artikel und Beiträge waren und auch in dieser Ausgabe des JavaSPEKTRUMs mit eigenen Beiträgen gewürdigt werden, stellt diese Ausgabe des Praktikers Ihnen sechs eher unbekannte neue Features vor.
Stream-Erweiterungen
Das mit Java 8 eingeführte Stream-API erhält vier neue erwähnenswerte Methoden: takeWhile und dropWhile sowie eine überladene iterate-Methode und ofNullable.
takeWhile und dropWhile helfen, auf einem vorhanden Stream nur eine Teilmenge zu verarbeiten. takeWhile verarbeitet auf einem geordneten Stream (s. Kasten „Streams und Ordnung“) alle Elemente solange, bis das übergebene Predicate true ergibt. Anschließend ist der Stream zu Ende. Sollte der Stream ungeordnet sein, so ist das Verhalten der Methode nicht genau spezifiziert. Eine Ausnahme ist der Fall, in dem alle Elemente verarbeitet werden, dies muss auch für einen ungeordneten Stream sichergestellt werden.
Streams und Ordnung
Ich bin bei der Erstellung dieses Artikels darüber gestolpert, dass es auch ungeordnete Streams geben kann [7]. Das für uns relevante Merkmal ist das deterministische Verhalten. Es gibt Streams, die bei mehrmaliger Verarbeitung dieses nicht aufweisen. Dies führt dazu, dass bei der Verarbeitung von ungeordneten Streams jeder Durchlauf zu einem anderen Ergebnis führen kann.
Im Gegensatz hierzu sorgt dropWhile dafür, dass solange alle Elemente verworfen werden, bis das übergebene Predicate das erste mal false zurückgibt. Auch für diese Methode gilt, dass ihr Ergebnis für ungeordnete Streams nicht spezifiziert ist, außer alle Elemente werden verworfen.
Ein Beispiel für die Verwendung von take- und dropWhile zeigt Listing 1. Das Beispiel verwirft solange alle Elemente bis zum öffnenden Body-Tag. Anschließend überspringt es dieses Element und verarbeitet alle weiteren Elemente, bis das schließende Body-Tag auftaucht. Die Ausgabe enthält somit nur noch das h1- und p-Tag.
Die statische Methode iterate auf java.util.stream.Stream kann bereits seit Java 8 dazu genutzt werden, unendlich lange Streams zu erzeugen. Java 9 fügt nun eine überladene iterate-Methode hinzu, mit der endliche Streams erzeugt werden können (s. Listing 2).
Eine praktische Verwendung hiervon ist vor allem an Stellen gegeben, die mit Enumeration arbeiten. Listing 3 zeigt, wie man mittels iterate alle JVM-Properties ausgeben kann.
Die vierte neue Methode ofNullable hilft an allen Stellen, an denen null zurückgegeben werden kann und anschließend über eine Collection iteriert werden soll (s. Listing 4). Ähnliches konnte auch in Java 8 bereits über Optional::ofNullable erreicht werden, man spart sich jedoch den Aufruf von .orElse(Stream.empty()).
Optionale Erweiterungen
Auch java.util.Optional wird mit Java 9 um einige hilfreiche Dinge erweitert. So lässt sich durch die neue Methode stream() jetzt einfach aus einem Optional ein Stream erzeugen, der entweder leer ist oder genau ein Element enthält. Viele der hierzu passenden Anwendungsfälle lassen sich zwar auch schon in Java 8 mit Aufrufen von map oder flatMap lösen, an manchen Stellen ist die Verwendung von stream jedoch eleganter. Zudem werden die Operationen auf einem Stream erst bei einem Aufruf einer terminierenden Methode ausgeführt (s. Listing 5).
Als zweite Neuerung gibt es nun neben orElse auch die Methode or. or bekommt als Argument einen Supplier übergeben, welcher ein Optional vom gleichen Typ erzeugen muss. Besonders hilfreich ist dieses Muster, wenn wir Daten aus verschiedenen Quellen bekommen können (s. Listing 6).
Die dritte und letzte Neuerung in Optional stellt die Methode ifPresentOrElse dar. Ziel dieser Methode ist es, Fälle zu vereinfachen, in denen etwas anderes passieren soll, wenn ein leeres Optional vorliegt. Sie ergänzt somit die Methode ifPresent (s. Listing 7).
Process-API-Erweiterungen
Unter dem JDK Enhancement Proposal 102 [8] wird das bereits existierende Process-API (java.lang.Process) erweitert. Eines der Ziele ist hierbei, sowohl für den JVM-Prozess, in dem man selber läuft, als auch für Prozesse, die man selber startet, mehr Informationen zur Verfügung zu stellen. Wollte man zum Beispiel bisher innerhalb seines eigenen Codes wissen, unter welcher Prozess-ID (PID) die JVM läuft, ließ sich dies nur über Umwege (z. B. Native Code per JNI) herausfinden. Mit Java 9 ist dies nun einfach und standardisiert auf jedem Betriebssystem möglich (s. Listing 8).
Neben der PID kommt man über einen Aufruf der Methode ProcessHandle::info() auch an detaillierte Informationen, wie den User, unter dem der Prozess läuft, wann der Prozess gestartet wurde, wie viel CPU-Zeit er bisher verbraucht hat und auch welches Kommando mit welchen Argumenten zum Starten verwendet wurde. Zur bequemen Verwendung wurde der vorhandene java.lang.Process so erweitert, dass man diesen nicht immer in ein ProcessHandle umwandeln muss. Neben dem aktuellen Prozess bietet ProcessHandle auch die Möglichkeit, an alle aktuell laufenden Prozesse zu gelangen (s. Listing 9).
Zusätzlich geben einem die Methoden parent(), children() und descendants() die Möglichkeit, sich durch den Prozessbaum zu hangeln. Ein Finden aller Root-Prozesse auf dem System lässt sich somit wie in Listing 10 implementieren.
Neben dem reinen Abfragen von Prozessinformationen bietet uns das erweiterte API auch die Möglichkeit, mit destroy() und destroyForcibly() Prozesse zu beenden. Der eigene Prozess, also die JVM, in der man selber läuft, lässt sich dabei nicht beenden (s. Listing 11).
Aus Sicherheitsgründen möchte man natürlich die Möglichkeit haben, dieses API für den Nutzer einzuschränken. Dafür prüft die Programmierschnittstelle über den SecurityManager der JVM den Wert der Runtime-Permission manageProcess. Ist dieses Recht nicht vorhanden, wird verhindert, dass der Nutzer eine ProcessHandle-Instanz erlangen kann. Feingranularer kann dieses API nicht abgesichert werden. Es greifen jedoch natürlich noch die vom Betriebssystem bereitgestellten Sicherheitskonzepte. Somit sind hier nicht mehr Möglichkeiten vorhanden, als ein nativer Prozess auch hätte.
Factory-Methoden für Collections
Häufig wird an den Java Collections kritisiert, dass die Erzeugung zu kompliziert sei. Diesem Thema widmet sich innerhalb von Java 9 der JEP 269 [9]. Dieser spezifiziert für List, Set und Map statische Methoden, die diese erzeugen (s. Listing 12).
Hierbei ist zu beachten, dass alle hiermit erzeugten Collections Immutable sind. Dieser JEP hat sich explizit nicht zum Ziel gesetzt, ein großes Builder-API für alle Arten von Collections zu sein. Aus diesem Grunde werden hier auch nicht bereits bekannte Implementierungen zurückgegeben, sondern es wurden neue implementiert. Diese Implementierungen sind speziell auf Collections mit wenigen Elementen abgestimmt und können bei größeren Mengen unperformantes Verhalten aufweisen.
Neues Versionsnummernschema
Seit der Übernahme von Java durch Oracle wurde das Versionierungsschema bereits mehrfach geändert. Allerdings haben diese Änderungen wenig zum Verständnis beigetragen. Aktuell gibt es beispielsweise das Oracle Java 8 JDK in den beiden Versionen 8u101 und 8u102. Ich muss bei jedem Update erst einmal nachgucken, welche von den beiden Versionen ich haben möchte und was genau der Unterschied ist.
Genau diese Probleme zu beseitigen, hat sich JEP 223 [10] zum obersten Ziel gesetzt. Die Versionsnummer besteht nun aus einer nicht leeren Menge von Elementen, die durch Punkte getrennt werden. Jedes Element ist hierbei entweder 0 oder eine ganze positive Zahl ohne führende Nullen. Zudem ist das letzte Element niemals 0. Weiterhin haben die ersten drei Elemente der Versionsnummer eine spezielle Bedeutung.
Das erste Element der Versionsnummer ist der Major-Teil. Für Java 9 ist dies die
- Erhöht wird dieses Element bei jedem Release, das signifikante Änderungen beinhaltet. Zudem sind bei einer Erhöhung dieses Elementes auch inkompatible Änderungen erlaubt und es kann angekündigt werden, dass gewisse Features in Zukunft, das heißt frühestens mit dem nächsten Major-Release, entfernt werden. Wird dieses Element erhöht, so werden alle folgenden Elemente auf 0 gesetzt und somit entfernt.
Als zweites Element kann ein Minor-Teil auftauchen. Dieser Teil wird immer dann erhöht, wenn kleinere kompatible Änderungen oder Updates von APIs vorgenommen werden.
Durch das dritte Element wird das Security-Level angezeigt. Dieser Teil wird mit jedem Sicherheitsupdate erhöht und wird nicht auf 0 gesetzt, wenn es eine neue Minor-Version gibt. Somit kann man alleine anhand dieses Elementes sehen, welche von zwei Versionen desselben Major-Releases mehr Sicherheitsupdates erhalten hat.
Die erste Java 9-Version sollte somit die Versionsnummer 9 erhalten.
Neben der Versionsnummer wird auch noch ein sogenannter Versionsstring definiert. Dieser besteht aus der Versionsnummer, dem Pre-Release, der Build-Nummer und gegebenenfalls auch noch weiteren Informationen. So ergibt die Ausgabe für das aktuelle Early Access Release: 9-ea+128.
Die nächste durch diesen JEP spezifizierte Änderung ergibt sich automatisch durch diese Regeln. Java 9 wird das erste Release, das nicht mehr mit 1.x beginnt. Bemerkenswert ist diese Änderung, da sie die durchaus reale Gefahr beinhaltet, dass Code, der bisher geschrieben wurde, um Java-Versionen zu vergleichen, nicht mehr funktioniert [11], oder Tooling von Entwicklern gestört wird. Siehe Listing 13 für die unter Mac gebräuchlichen Bash-Aliasse, um die Java-Version zu ändern. Bereits hier hat die Änderung der Versionsnummer eine Auswirkung.
Neben der reinen Definition bietet Java 9 mittels java.lang.Runtime.Version auch eine Programmierschnittstelle an, um Java-Versionsnummern zu parsen und zu vergleichen (s. Listing 14)
Zu guter Letzt standardisiert dieser JEP noch die Rückgabewerte für die folgenden fünf System-Properties:
- java.version
- java.runtime.version
- java.vm.version
- java.specification.version
- java.vm.specification.version
Multi-Release-JAR-Dateien
Die letzte Neuerung, die ich Ihnen in dieser Kolumne vorstellen möchte, wird im JEP 238 [12] spezifiziert. Die Umsetzung dieses JEPs erlaubt, dass man innerhalb einer JAR-Datei kompilierte Class-Dateien hat, die nur in bestimmten Java-Versionen genutzt werden.
Für Applikationen hat dieses Feature nur wenig Relevanz, Bibliotheken hingegen können hiervon profitieren. Beispielsweise könnte man, um die aktuelle PID abzufragen, ab Java 9 die neuen Features des Process-API nutzen, gleichzeitig jedoch für ältere JDKs andere Wege einschlagen.
Hierzu muss das Manifest in der JAR-Datei das Property Multi-Release auf true setzen. Anschließend können verschiedene Versionen derselben Klassen ausgeliefert werden. Listing 15 zeigt, wie eine solche JAR-Datei aussieht.
Wird dieses JAR mit Java 9 verwendet, wird die spezifisch für Java 9 abgelegte Klasse A, die für Java 8 erstellte Klasse B und die von allen Versionen genutzte Klasse C verwendet. Mit Java 8 wird die dort abgelegte Klasse A genutzt. Alle älteren Java-Runtimes (und damit alle, die dieses Format nicht unterstützen) nutzen nur die im Root gefundenen Klassen. Ein Beispiel, wie dies aktuell mit Maven machbar ist, finden Sie im Projekt hboutemy/maven-jep238 [13].
Fazit
Leider hat auch diese Kolumne nur Platz, um fünf kleine und eher unbekannte Features vorzustellen. Die Webseite zum JDK 9 [1] listet alle JEPs auf und ist einen Blick wert. Ich bin mir sicher, Sie finden dort noch weitere interessante Neuerungen.
Ich hoffe, ich konnte Ihnen noch die ein oder andere bisher unbekannte Neuerung vorstellen, und freue mich wie immer auf Feedback.
Enthaltene Beispiele
Sämtliche Beispiele dieser Kolumne können natürlich von Ihnen ausprobiert werden. Dazu finden Sie sämtliche Sourcen unter https://github.com/mvitz/javaspektrum-java9.
Diese wurden mit Build 128 des JDK 9 EA Releases gebaut und nutzen Apache Maven 3.3.3 als Build-Tool. Nach einem Import dieses Projektes in meine IDE (IntelliJ IDEA 2016.2) und dem Hinzufügen des JDKs hat alles ohne weitere Konfiguration funktioniert.