Die wichtigsten neuen Funktionen wurden wieder in Form von Java Enhancement Proposals (JEP) entwickelt. Eine Auflistung inklusive Verlinkungen zu den einzelnen Projektseiten findet sich auf der Webseite des OpenJDK.
- 339: Edwards-Curve Digital Signature Algorithm (EdDSA)
- 360: Sealed Classes (Preview)
- 371: Hidden Classes
- 372: Remove the Nashorn JavaScript Engine
- 373: Reimplement the Legacy DatagramSocket API
- 374: Disable and Deprecate Biased Locking
- 375: Pattern Matching for instanceof (Second Preview)
- 377: ZGC: A Scalable Low-Latency Garbage Collector
- 378: Text Blocks
- 379: Shenandoah: A Low-Pause-Time Garbage Collector
- 381: Remove the Solaris and SPARC Ports
- 383: Foreign-Memory Access API (Second Incubator)
- 384: Records (Second Preview)
- 385: Deprecate RMI Activation for Removal
Einige der Features befinden sich noch im Preview- oder Inkubator-Modus. Die Macher des OpenJDKs wollen ihre Ideen so möglichst frühzeitig vorzeigen und erhoffen sich Feedback aus der Community. Diese Rückmeldungen fließen dann bereits in die nächsten halbjährlichen Releases ein. Wir Java-Entwickler können zudem regelmäßig neue Sprachfunktionen und JDK-Erweiterungen ausprobieren. Spätestens zur nächsten LTS-(Long-Term-Support-)Version erfolgt dann die Finalisierung der Previews. Das wird übrigens das OpenJDK 17 sein, das für September 2021 geplant ist.
Auf JAXenter gibt es bereits einen kompakten Überblick über alle Änderungen des JDK 15. Wir wollen in diesem Artikel etwas tiefer auf die vor allem für Entwickler relevanten Themen schauen. Und da sind die Sealed Classes (JEP 360) die vermutlich interessanteste Neuerung. Dieses Feature wurde im Rahmen von Projekt Amber entwickelt. Sealed Classes gehören zu einer Reihe von vorbereitenden Maßnahmen für die Umsetzung von Pattern Matching in Java. Ganz konkret sollen sie bei der Analyse von Mustern unterstützen. Aber auch für Framework-Entwickler bieten sie einen interessanten Mehrwert. Die Idee ist, dass versiegelte Klassen und Interfaces entscheiden können, welche Subklassen oder -Interfaces von ihnen abgeleitet werden dürfen. Bisher konnte man als Entwickler Ableitung von Klassen nur durch Zugriffsmodifikatoren (private, protected, …) einschränken oder durch die Deklaration der Klasse als final komplett durch den Compiler untersagen. Sealed Classes bieten nun einen deklarativen Weg, um nur bestimmten Subklassen die Ableitung zu erlauben. Ein Beispiel:
public sealed class Vehicle permits Car, Bike, Bus, Train { }
Vehicle darf nur von den vier genannten Klassen überschrieben werden. Damit wird auch dem Aufrufer deutlich gemacht, welche Subklassen erlaubt sind und damit überhaupt existieren. Subklassen bergen zudem immer die Gefahr, dass beim Überschreiben der Vertrag der Superklasse verletzt wird. Zum Beispiel ist es unmöglich, die Bedingungen der equals-Methode aus der Klasse Object zu erfüllen, wenn man Instanzen von einer Super- und einer Subklasse miteinander vergleichen will. Weitere Details dazu kann man in der API-Dokumentation unter dem Stichwort Äquivalenzrelationen, konkret Symmetrie nachlesen.
Sealed Classes funktionieren auch mit abstrakten Klassen und integrieren sich zudem gut mit den in der Vorgängerversion eingeführten Record-Datentypen. Es gibt aber ein paar Einschränkungen. Eine Sealed Class und alle erlaubten Subklassen müssen im selben Modul existieren. Im Fall von Unnamed Modules müssen sie sogar im gleichen Package liegen. Außerdem muss jede erlaubte Subklasse direkt von der Sealed Class ableiten. Die Subklassen dürfen übrigens wieder selbst entscheiden, ob sie weiterhin versiegelt, final oder komplett offen sein wollen. Die zentrale Versiegelung einer ganzen Klassenhierarchie von oben bis zur untersten Hierarchiestufe ist leider nicht möglich. Weitere Details finden sich auf der Projektseite des JEPs 360.
Das zweite Mal dabei, sozusagen im Recall, ist das bereits in Java 14 als Preview eingeführte Pattern Matching for instanceof. Ein Pattern ist eine Kombination aus einem Prädikat, das auf eine Zielstruktur passt, und einer Menge von Variablen innerhalb dieses Musters. Diesen Variablen werden bei passenden Treffern die entsprechenden Inhalte zugewiesen und damit extrahiert. Die Intention des Pattern Matching ist letztlich die Destrukturierung von Objekten, also das Aufspalten in die Bestandteile und Zuweisen in einzelne Variablen zur weiteren Bearbeitung. Die Spezialform des Pattern Matching beim instanceof-Operator spart unnötige Casts auf die zu prüfenden Zieldatentypen. Wenn o ein String oder eine Collection ist, dann kann direkt mit den neuen Variablen (s und c) mit den entsprechenden Datentypen weitergearbeitet werden. Das Ziel ist es, Redundanzen zu vermeiden und dadurch die Lesbarkeit zu erhöhen:
boolean isNullOrEmpty( Object o ) { return o == null || o instanceof String s && s.isBlank() || o instanceof Collection c && c.isEmpty(); }
Der Unterschied zum zusätzlichen Cast mag marginal erscheinen. Für die Puristen unter den Java-Entwicklern spart das allerdings eine kleine, aber dennoch lästige Redundanz ein. Laut Brian Goetz soll die Sprache Java dadurch prägnanter und die Verwendung sicherer gemacht werden. Erzwungene Typumwandlungen werden vermieden und dafür implizit durchgeführt. Die zweite Preview bringt übrigens keine Änderungen seit dem JDK 14 mit sich. Es soll aber noch einmal die Möglichkeit gegeben werden, Feedback abzugeben. In zukünftigen Java-Versionen wird es das Pattern Matching dann auch für weitere Sprachkonstrukte geben, z. B. innerhalb von Switch Expressions.
Ebenfalls das zweite Mal dabei sind die Records, eine eingeschränkte Form der Klassendeklaration, ähnlich den Enums. Entwickelt wurden Records im Rahmen des Projekts Valhalla. Es gibt gewisse Ähnlichkeiten zu Data Classes in Kotlin und Case Classes in Scala. Die kompakte Syntax könnte Bibliotheken wie Lombok in Zukunft obsolet machen. Die einfache Definition einer Person mit zwei Feldern sieht folgendermaßen aus:
public record Person(String name, Person partner ) {}
Das nächste Listing zeigt eine erweiterte Variante mit einem zusätzlichen Konstruktor. Dadurch lassen sich neben Pflichtfeldern auch optionale Felder abbilden:
public record Person(String name, Person partner ) { public Person(String name ) { this( name, null ); } public String getNameInUppercase() { return name.toUpperCase(); } }
Erzeugt wird vom Compiler eine unveränderbare (immutable) Klasse, die neben den beiden Attributen und den eigenen Methoden natürlich auch noch die Implementierungen für die Accessoren, den Konstruktor sowie equals/hashCode und toString enthält (Listing 1).
Listing 1
public final class Person extends Record { private final String name; private final Person partner; public Person(String name) { this(name, null); } public Person(String name, Person partner) { this.name = name; this.partner = partner; } public String getNameInUppercase() { return name.toUpperCase(); } public String toString() { /* ... */ } public final int hashCode() { /* ... */ } public final boolean equals(Object o) { /* ... */ } public String name() { return name; } public Person partner() { return partner; } }
Verwendet werden Records dann wie normale Java-Klassen. Der Aufrufer merkt also gar nicht, dass ein Record-Typ instanziiert wird (Listing 2).
Listing 2
var man = new Person("Adam"); var woman = new Person("Eve", man); woman.toString(); // ==> "Person[name=Eve, partner=Person[name=Adam, partner=null]]" woman.partner().name(); // ==> "Adam" woman.getNameInUppercase(); // ==> "EVE" // Deep equals new Person("Eve", new Person("Adam")).equals( woman ); // ==> true
Records sind übrigens keine klassischen JavaBeans, da sie keine echten Getter enthalten. Man kann auf die Member-Variablen aber über die gleichnamigen Methoden zugreifen (name() statt getName()). Records können im Übrigen auch Annotationen oder JavaDocs enthalten. Im Body dürfen zudem statische Felder sowie Methoden, Konstruktoren oder Instanzmethoden deklariert werden. Nicht erlaubt ist die Definition von weiteren Instanzfeldern außerhalb des Record Headers.
Die zweite Preview der Records in Java 15 enthält einige kleinere Verbesserungen aufgrund des Feedbacks aus der Community. Außerdem gibt es eine Integration der Records mit den Sealed Classes, wie das aus der Dokumentation des JEP 384 entnommene Beispiel zeigt (Listing 3).
Listing 3
public sealed interface Expr permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... } public record ConstantExpr(int i) implements Expr {...} public record PlusExpr(Expr a, Expr b) implements Expr {...} public record TimesExpr(Expr a, Expr b) implements Expr {...} public record NegExpr(Expr e) implements Expr {...}
Eine Familie von Records kann von dem gleichen Sealed Interface ableiten. Die Kombination aus Records und versiegelten Datentypen führt uns zu algebraischen Datentypen, die vor allem in funktionalen Sprachen wie Haskell zum Einsatz kommen. Konkret können wir jetzt mit Records Produkttypen und mit versiegelten Klassen Summentypen abbilden.
Eine weitere Neuerung sind geschachtelte Records, um schnell und einfach Zwischenergebnisse in Form von zusammengehörigen Variablen modellieren zu können. Bisher konnte man bereits statische innere Records (analog zu statischen inneren Klassen) deklarieren. Jetzt gibt es auch die Möglichkeit, lokale Records innerhalb von Methoden als temporäre Datenstrukturen zu erzeugen. Listing 4 zeigt ein Beispiel.
Listing 4
List<Merchant> findTopMerchants(List<Merchant> merchants, int month) { // Local record record MerchantSales(Merchant merchant, double sales) {} return merchants.stream() .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month))) .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales())) .map(MerchantSales::merchant) .collect(toList()); }
Ebenfalls zum zweiten Mal dabei ist das Foreign-Memory Access API (JEP 383). Damit sollen Java-Programme sicher und effizient Speicher außerhalb des Heap verwalten können. Prominente Beispiele sind In-Memory-Lösungen wie Ignite, mapDB, Memcached oder das ByteBuf API von Netty. Diese neue Bibliothek löst die veraltete Alternative sun.misc.Unsafe ab und macht Workarounds über java.nio.ByteBuffer überflüssig. Es ist Teil des Projekts Panama, das die Verbindungen zwischen Java- und Nicht-Java-APIs verbessern will.
Diesmal nicht als eigenes JEP aufgelistet, aber trotzdem wieder mit von der Partie sind die erst in Java 14 eingeführten Helpful NullPointerExceptions. Inhaltlich gab es keine Änderungen. Allerdings sind sie nun direkt aktiv und müssen nicht mehr explizit über einen Kommandozeilenparameter eingeschaltet werden. Sie sind sehr hilfreich bei auftretenden NullPointerExceptions, weil sie die betroffene Variable oder den verursachenden Methodenaufruf direkt benennen. Als Entwickler muss man nicht mehr raten oder durch aufwendiges Debuggen die betroffene Stelle ermitteln.
Dem Preview entwachsen sind in Java 15 die Text Blocks. Eigentlich war bereits im JDK 12 mit den Raw String Literals eine größere Änderung bei der Verarbeitung von Zeichenketten angekündigt, musste dann aber aufgrund von Unstimmigkeiten innerhalb der Community zurückgezogen werden. Im OpenJDK 13 und 14 wurden dann die Text Blocks als abgespeckte Variante in Form eines Preview-Features eingeführt. Ein Text Block ist dabei ein mehrzeiliges String Literal. Zeilenumbrüche werden nicht durch eine den Lesefluss störende Escape-Sequenz eingeleitet. Der String wird automatisch, aber auf nachvollziehbare Art und Weise formatiert. Wenn nötig, kann der Entwickler aber in die Formatierung eingreifen. Insbesondere für HTML-Templates und SQL-Skripte erhöht sich die Lesbarkeit enorm (Listing 5).
Listing 5
String html = "<html>// Ohne Text Blocks \n" + " <body>\n" + " <p>Hello, Escapes</p>\n" + " </body>\n" + "</html>\n"; // Mit Text Blocks String html = """ <html> <body> <p>Hello, Text Blocks</p> </body> </html>""";
In Java 14 gab es noch kleinere Änderungen, u. a. wurden zwei neue Escape-Sequenzen hinzugefügt, um die Formatierung einer mehrzeiligen Zeichenkette anpassen zu können. Nun sind Text Blocks als offizielles Feature in den Java-Sprachumfang aufgenommen.
Aber es kamen nicht nur neue Features hinzu. Seit dem JDK 11 müssen wir auch immer wieder damit rechnen, dass als deprecated markierte Funktionen und Bibliotheken wegfallen. Diesmal hat es die erst in Java 8 eingeführte JavaScript-Engine Nashorn erwischt. Die mit ECMAScript 5.1 kompatible Implementierung hatte das frühere Projekt Rhino abgelöst, konnte sich aber nie annäherungsweise gegen die etablierte Plattform Node.js behaupten. Zudem entwickeln sich JavaScript und der ECMA-Standard sehr rapide weiter und es wäre sehr viel Aufwand, die Engine im JDK auf Stand zu halten. Keinen Einfluss hat der Ausbau übrigens auf das API javax.script, mit dem man beliebige Skriptsprachen aus einer Java-Anwendung heraus starten kann. Möchte man in Zukunft direkt auf der JVM JavaScript-Code ausführen, sollte man sich auch die polyglotte GraalVM näher anschauen. Damit lassen sich auch noch verschiedene andere Sprachen wie Ruby, Python usw. ausführen.
Ebenfalls entfallen werden die JVM-Portierungen für Solaris/SPARC. Diese sind noch ein Überbleibsel aus den Sun-Zeiten. Frühere Java-Versionen (bis einschließlich JDK 14) bleiben auf den alten Systemen weiterhin lauffähig. Falls der Bedarf besteht und sich interessierte Entwickler finden, können diese Varianten auch wiederbelebt werden. Im Moment möchte Oracle die freigewordenen Kapazitäten aber nutzen, um andere Features voranzutreiben.
Bei den Garbage Collectors hat sich in den vergangenen Jahren ebenfalls viel getan. Die automatische Speicherbereinigung war in den Anfangsjahren von Java eines der Killerargumente, mit dem man sich von der Konkurrenz abgehoben hat. Die Wahl des Garbage-Collector-Algorithmus hat natürlich direkt Einfluss auf die Performance einer Anwendung. Es gab schon immer verschiedene Implementierungen, aus denen man wählen konnte und die je nach Szenario mehr oder weniger gut geeignet waren. Durch die Weiter- und Neuentwicklung von Garbage Collectors konnte bereits in den letzten Jahren immer wieder an der Performanceschraube gedreht werden. So wurden beim Umstieg von Java 8 auf 11 von vielen Entwicklern signifikante Performanceverbesserungen gemessen, ohne eine Zeile Code geändert zu haben. Der Hintergrund ist der Wechsel des Standard-Garbage-Collectors zum G1.
Mit dem OpenJDK 15 hat sich nun der Status des ZGC von Experimental- zum Production- Feature geändert (JEP 377). Der ZGC wird als skalierbarer Garbage Collector mit niedriger Latenz angepriesen. Er bringt eine Reduzierung der GC-Pausenzeiten mit sich und kann mit beliebig großen Heap-Speichern umgehen (von wenigen Megabytes bis hin zu Terabytes). Ebenfalls in den Production-Status überführt wurde der in Java 12 erstmals eingeführte und ursprünglich bei Red Hat entwickelte Shenandoah GC (JEP 379). Weder ZGC noch Shenandoah sollen übrigens den aktuellen Standard-GC (G1) ersetzen. Sie bieten einfach eine Alternative, die bei bestimmten Szenarien performanter sein kann.
Ein „Hidden Feature“, das aber eher für Bibliothek- und Framework-Entwickler interessant ist, wurde mit dem JEP 371 hinzugefügt. Ziel der sogenannten Hidden Classes ist es, dynamisch zur Laufzeit erstellte Klassen leichtgewichtiger und sicherer zu machen. Bei den bisherigen Mechanismen (ClassLoader::defineClass und Lookup::defineClass) gab es keinen Unterschied, ob der Bytecode der Klasse dynamisch zur Laufzeit oder statisch beim Kompilieren entstanden ist. Damit waren dynamisch erzeugte Klassen sichtbarer als notwendig. Hidden Classes können weder von anderen Klassen eingesehen oder verwendet werden noch sind sie über Reflection auffindbar. Als ein Anwendungsfall könnte java.lang.reflect.Proxy versteckte Klassen definieren, die dann als Proxy-Klassen fungieren. Zu beachten ist bei Hidden Classes aber das Problem, dass sie derzeit (noch) nicht in Stacktraces auftauchen. Das erschwert natürlich eine notwendige Fehlersuche.
Fazit
Java 15 macht wieder einen grundsoliden Eindruck. Oracle hat das nun nicht mehr ganz so neue Releasemanagement weiterhin gut im Griff und konnte erneut pünktlich liefern. Im März 2021 steht mit Java 16 schon das nächste Major-(Zwischen-)Release auf dem Plan, bevor dann im Herbst mit der Version 17 erneut eine Version mit Long Term Support folgt. Für das OpenJDK 16 wurden, Stand jetzt, bereits fünf JEPs eingeplant und drei weitere vorgeschlagen. Unter anderem geht es um den sich bereits in Arbeit befindlichen Wechsel bei der Versionsverwaltung (Umstieg von Mercurial auf Git) und den Umzug der OpenJDK-Quellen nach GitHub. Bis zum Feature-Freeze im Dezember werden noch weitere API-Erweiterungen und neue Features folgen. Wir dürfen gespannt sein, was uns erwarten wird. Genügend Stoff liefern die Inkubatorprojekte Loom, Amber, Valhalla und andere auf jeden Fall.
Auch mit 25 Jahren gehört die Programmiersprache Java noch längst nicht zum alten Eisen. Im Ranking des Tiobe-Index ist Java im September 2020 zwar nur noch auf dem zweiten Platz. Aber das ändert sich regelmäßig, und der Langzeittrend zeigt, dass Java in den letzten 20 Jahren immer unter den Top 3 gelandet ist. Im Moment deutet nichts darauf hin, dass sich daran in den nächsten Jahren etwas ändern könnte.
Spring Ecosystem auf der JAX & W-JAX:
● Workshop: Coole neue Java-Features – besserer Code mit Java 9 bis 15
● Neues in Java