Born to write Code

Java 14: Die Neuerungen der aktuellen Version auf einem Blick

Falk Sippach

© Software & Support Media GmbH / Bianca Röder

„Languages must evolve, or they risk becoming irrelevant“, sagte Brian Goetz (Oracle) im November 2019 während seiner Präsentation „Java Language Futures“ bei der Devoxx in Belgien. Er ist als Java Language Architect maßgeblich daran beteiligt, dass Java trotz seiner 25 Jahre noch lange nicht zum alten Eisen gehört. In diesem Artikel werfen wir einen Blick auf die Neuerungen des JDK 14.

Oracle hat in den vergangenen Jahren einige wegweisende Entscheidungen getroffen. Dazu zählt das neue, halbjährliche Releasemodell mit den Preview-Features und den kürzeren Veröffentlichungs- und Feedbackzyklen für neue Funktionen. Das Lizenzmodell wurde geändert, das Oracle JDK wird nicht mehr kostenfrei angeboten. Das hat den Wettbewerb angekurbelt und so bekommt man nun von diversen Anbietern, auch von Oracle, freie Distributionen des OpenJDK. Das ist seit Java 11 binärkompatibel zum Oracle JDK und steht unter einer Open-Source-Lizenz.

Vor anderthalb Jahren ist im Herbst 2019 mit Java 11 die letzte LTS-Version erschienen. Seitdem gab es bei den beiden folgenden Major-Releases jeweils nur eine überschaubare Menge an neuen Features. In den JDK-Inkubator-Projekten (Amber, Valhalla, Loom …) wird aber bereits an vielen neuen Ideen gearbeitet und so verwundert es nicht, dass der Funktionsumfang beim gerade veröffentlichten JDK 14 wieder deutlich größer ausfällt. Und auch wenn nur wenige die neue Version in Produktion einsetzen werden, sollte man trotzdem frühzeitig einen Blick auf die Neuerungen werfen und ggf. Feedback zu den Preview-Funktionen geben. Nur so wird sichergestellt, dass die neuen Features bis zur Finalisierung im nächsten LTS-Release, das als Java 17 im Herbst 2021 erscheinen wird, produktionsreif sind.

Java Whitepaper

Gratis-Dossier: Java 2020 – State of the Art

GraalVM, Spring Boot 2.2, Kubernetes, Domain-driven Design & Machine Learning sind einige der Themen, die Sie in unserem brandneuen Dossier 2020 wiederfinden werden!

Die folgenden Java Enhancement Proposals (JEP) wurden umgesetzt (siehe [1]). Wir wollen in diesem Artikel die aus Entwicklersicht interessanten Themen genauer unter die Lupe nehmen.

  • JEP 305: Pattern Matching for instanceof (Preview)
  • JEP 343: Packaging Tool (Incubator)
  • JEP 345: NUMA-Aware Memory Allocation for G1
  • JEP 349: JFR Event Streaming
  • JEP 352: Non-Volatile Mapped Byte Buffers
  • JEP 358: Helpful NullPointerExceptions
  • JEP 359: Records (Preview)
  • JEP 361: Switch Expressions (Standard)
  • JEP 362: Deprecate the Solaris and SPARC Ports
  • JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
  • JEP 364: ZGC on macOS
  • JEP 365: ZGC on Windows
  • JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
  • JEP 367: Remove the Pack200 Tools and API
  • JEP 368: Text Blocks (Second Preview)
  • JEP 370: Foreign-Memory Access API (Incubator)

JEP 305: Pattern Matching für instanceof

Das Pattern-Matching-Konzept kommt bereits seit den 1960er Jahren bei diversen Programmiersprachen zum Einsatz. Zu den moderneren Vertretern zählen unter anderem Haskell und Scala. 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. Die Intention ist die Destrukturierung von Objekten, also das Aufspalten in ihre Bestandteile.

Bisher konnte man in Java im Switch Statement nur nach den Datentypen Integer, String und Enum unterscheiden. Durch die Einführung der Switch Expression in Java 12 wurde aber bereits der erste Schritt hin zum Pattern Matching vollzogen. Mit Java 14 können wir nun zusätzlich Pattern Matching beim instanceof-Operator nutzen. Dabei werden unnötige Casts vermieden, zudem erhöht sich durch die verringerte Redundanz die Lesbarkeit.

Vorher musste man beispielsweise für das Prüfen auf einen leeren String bzw. eine leere Collection wie folgt vorgehen:

boolean isNullOrEmpty( Object o ) {
  return  o == null ||
    o instanceof String && ((String) o).isBlank() ||
    o instanceof Collection && ((Collection) o).isEmpty();
}

Jetzt kann man beim Check mit instanceof den Wert direkt einer Variablen zuweisen und darauf weitere Aufrufe ausführen:

boolean isNullOrEmpty( Object o ) {
  return o == null ||
    o instanceof String s && s.isBlank() ||
    o instanceof Collection c && c.isEmpty();
}

Der Unterschied mag marginal erscheinen. Die Puristen unter den Java-Entwicklern sparen damit allerdings eine kleine, aber dennoch lästige Redundanz ein.
Die Switch Expressions hatte man zunächst in Java 12 und 13 jeweils als Preview-Feature eingeführt. Sie wurden nun im JEP 361 finalisiert. Dadurch stehen den Entwicklern zwei neue Syntaxvarianten mit einer kürzeren, klareren und weniger fehleranfälligen Semantik zur Verfügung. Das Ergebnis der Expression kann einer Variablen zugewiesen oder als Wert aus einer Methode zurückgegeben werden (Listing 1). Weitere Details können dem Artikel zu Java 13 [2] entnommen werden.

String developerRating( int numberOfChildren ) {
  return switch (numberOfChildren) {
    case 0 -> "open source contributor";
    case 1, 2 -> "junior";
    case 3 -> "senior";
    default -> {
      if (numberOfChildren < 0) 
        throw new IndexOutOfBoundsException( numberOfChildren );
      yield "manager";
    }
  };
}

JEP 358: Helpful NullPointerExceptions

Der unbeabsichtigte Zugriff auf leere Referenzen ist auch bei Java-Entwicklern gefürchtet. Nach eigener Aussage von Sir Tony Hoare war seine Erfindung der Nullreferenz ein Fehler mit Folgen in Höhe von vielen Milliarden Dollar. Und das nur, weil es bei der Entwicklung der Sprache Algol in den 60er Jahren einfach so leicht zu implementieren war.

In Java gibt es weder vom Compiler noch von der Laufzeitumgebung Unterstützung beim Umgang mit Nullreferenzen. Mit diversen Workarounds lassen sich diese leidigen Exceptions vermeiden. Der einfachste Weg stellt die Prüfungen auf null dar. Leider ist dieses Vorgehen sehr mühselig und wird immer genau dann vergessen, wenn man den Check gebraucht hätte. Mit der seit dem JDK 8 enthaltenen Wrapper-Klasse Optional kann man über das API explizit den Aufrufer darauf hinweisen, dass ein Wert null sein kann und er darauf reagieren muss. Somit kann man nicht mehr aus Versehen in eine Nullreferenz hineinlaufen, sondern muss explizit mit dem möglicherweise leeren Wert umgehen. Dieses Vorgehen bietet sich unter anderem bei Rückgabewerten von öffentlichen Schnittstellen an, kostet aber auch eine extra Indirektionsschicht, da man den eigentlichen Wert immer auspacken muss.

In anderen Sprachen wurden längst Hilfsmittel in Syntax und Compiler eingebaut, wie zum Beispiel bei Groovy das NullObjectPattern und der Safe Navigation Operator (some?.method()). Bei Kotlin kann man explizit zwischen Typen, die nicht leer sein dürfen und solchen, bei deren Referenz auch null erlaubt ist, unterscheiden. Mit den NullPointerExceptions werden wir in Java auch künftig leben müssen. Aber immerhin erleichtern uns die als Preview-Feature eingeführten Helpful NullPointerExceptions nun die Fehlersuche im Ausnahmefall. Damit beim Werfen einer NullPointerException die notwendigen Informationen eingefügt werden, muss man beim Starten die Option -XX:+ShowCodeDetailsInExceptionMessages aktivieren. Ist dann in einer Aufrufkette ein Wert null, bekommt man eine aussagekräftige Meldung:

man.partner().name()

Result: java.lang.NullPointerException: Cannot invoke "Person.name()" because the return value of "Person.partner()" is null

Bei Lambdaausdrücken braucht es eine Spezialbehandlung. Ist zum Beispiel der Parameter einer Lambdafunktion null, bekommt man standardmäßig die in Listing 2 gezeigte, unzureichende Fehlermeldung.

Stream.of( man, woman )
  .map( p -> p.partner() )
.map( p -> p.name() )
.collect( Collectors.toUnmodifiableList() );

Result: java.lang.NullPointerException: Cannot invoke "Person.name()" because "" is null

Damit der korrekte Parametername angezeigt wird, muss der Quellcode mit der Option -g:vars kompiliert werden. Das Resultat:

java.lang.NullPointerException: Cannot invoke "Person.name()" because "p" is null/

Bei Methodenreferenzen gibt es aktuell leider noch keinen Hinweis im Fall eines leeren Parameters:

Stream.of( man, woman )
  .map( Person::partner )
  .map( Person::name )
  .collect( Collectors.toUnmodifiableList() )
Result: java.lang.NullPointerException

Setzt man aber wie hier im Beispiel jeden Stream-Methodenaufruf in eine neue Zeile, lässt sich die problematische Codezeile sehr schnell eingrenzen. Herausfordernd waren NullPointerExceptions bisher auch beim automatischen Boxing/Unboxing. Wird auch hier der Compilerparameter -g:vars aktiviert, bekommt man ebenfalls die neue hilfreiche Fehlermeldung (Listing 3).

int calculate() {
  Integer a = 2, b = 4, x = null;
  return a + b * x;
}
calculate();
Result: java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "x" is null

JEP 359: Records

Die wahrscheinlich spannendste und gleichzeitig auch überraschendste Neuerung dürfte die Einführung der Record-Typen sein. Sie wurden noch relativ spät in das Release von Java 14 aufgenommen. Dabei handelt es sich um 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. Kevlin Henney sieht außerdem noch folgenden Vorteil [3]:

„I think one of the interesting side effects of the Java record feature is that, in practice, it will help expose how much Java code really is getter/setter-oriented rather than object-oriented.“

Die einfache Definition einer Person mit zwei Feldern sieht man hier:

public record Person( String name, Person partner ) {}

Eine erweiterte Variante mit einem zusätzlichen Konstruktor, damit nur das Feld name Pflicht ist, lässt sich ebenfalls realisieren:

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 (keine Getter!), den Konstruktor sowie equals/hashcode und toString enthält (Listing 4).

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; }
}

Die Verwendung gestaltet sich erwartungsgemäß. Man sieht dem Aufrufer nicht an, dass Record-Typen instanziiert werden (Listing 5).

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 Java Beans, da sie keine echten Getter enthalten. Man kann aber über die gleichnamigen Methoden auf die Membervariablen zugreifen. 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. Weitere Details dazu finden sich im Artikel von Tim Zöller in der kommenden Ausgabe (5.2020) des Java Magazins.

JEP 368: Text Blocks

Ursprünglich als Raw String Literals bereits für Java 12 geplant, hat man dann in Java 13 zunächst eine abgespeckte Variante in Form von mehrzeiligen Strings namens Text Blocks eingeführt. Insbesondere für HTML-Templates und SQL-Skripte erhöht sich dadurch die Lesbarkeit enorm (Listing 6).

// Ohne Text Blocks
String html = "<html>\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>""";

Neu hinzugekommen sind jetzt zwei Escape-Sequenzen, mit denen man die Formatierung eines Text Blocks anpassen kann. Um zum Beispiel einen Zeilenumbruch zu verwenden, der aber nicht explizit in der Ausgabe erscheinen soll, kann man am Zeilenende einfach einen \ (Backslash) einfügen. Dadurch bekommt man einen String mit einer langen Zeile, im Quellcode dürfen aber für die Übersichtlichkeit Umbrüche verwendet werden (Listing 7).

String text = """
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """;
// statt
String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
                 "elit, sed do eiusmod tempor incididunt ut labore " +
                 "et dolore magna aliqua.";

Die neue Escape-Sequenz \s wird zu einem Leerzeichen übersetzt. Dadurch kann man beispielsweise erreichen, dass Leerzeichen am Zeilenende nicht automatisch abgeschnitten (getrimmt) werden und man eine feste Zeichenbreite je Zeile erhält:

String colors = """
    red  \s
    green\s
    blue \s
    """;

Was gibt es noch Neues?

Neben den beschriebenen Features, die hauptsächlich für Entwickler interessant sind, gibt es diverse andere Änderungen. Im JEP 352 wurde das FileChannel API erweitert, um die Erzeugung von MappedByteBuffer-Instanzen zu ermöglichen. Die arbeiten, im Gegensatz zum volatilen Speicher, dem RAM, auf nichtflüchtigen Datenspeichern (NVM, non-volatile Memory). Die Zielplattform ist allerdings Linux x64. Auch bei der Garbage Collection hat sich einiges getan. So wurde der Concurrent Mark Sweep (CMS) Garbage Collector entfernt. Dafür gibt es den ZGC jetzt auch für macOS und Windows.

Bei kritischen Java-Anwendung wird empfohlen, die Flight-Recording-Funktion in Produktion zu aktivieren. Der folgende Befehl startet eine Java-Anwendung mit Flight Recording und schreibt die Informationen in die recording.jfr, wobei immer die Daten eines Tages aufgehoben werden:

java \
-XX:+FlightRecorder \
-XX:StartFlightRecording=disk=true, \
filename=recording.jfr,dumponexit=true,maxage=1d \
-jar application.jar

Normalerweise kann man die Daten dann mit dem Tool JDK Mission Control (JMC) auslesen und analysieren. Neu im JDK 14 ist, dass man auch aus der Anwendung heraus asynchron die Events abfragen kann (Listing 8).

import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;

try ( var rs = new RecordingStream() ) {
  rs.enable( "jdk.CPULoad" ).withPeriod( Duration.ofSeconds( 1 ) );
  rs.onEvent( "jdk.CPULoad", event -> {
    System.out.printf( "%.1f %% %n", event.getFloat( "machineTotal" ) * 100 );
  });
  rs.start();
}

Im JDK 8 gab es das Tool javapackager, das aber leider mitsamt JavaFX in Version 11 aus Java entfernt wurde. In Java 14 wird nun der Nachfolger jpackage eingeführt (JEP 343: Packaging Tool), mit dem wieder eigenständige Java-Installationsdateien erstellt werden können. Ihre Basis ist die Java-Anwendung mitsamt einer Laufzeitumgebung. Das Tool baut aus diesem Input ein lauffähiges Binärartefakt, das sämtliche Abhängigkeiten enthält (Formate: msi, exe, pkg in a dmg, app in a dmg, deb und rpm).

Fazit

Java ist nicht tot, lang lebe Java! Die halbjährlichen OpenJDK-Releases tun der Sprache und der Plattform gut. Diesmal gab es sogar deutlich mehr neue Funktionen also noch bei Java 12 und 13. Und es gibt noch viele Features, die in zukünftigen Versionen auf ihren Einsatz warten. Uns Java-Entwicklern wird also nicht langweilig werden, die Zukunft sieht weiterhin rosig aus. Im September 2020 erwartet uns bereits Java 15.

Verwandte Themen:

Geschrieben von
Falk Sippach
Falk Sippach
Falk Sippach hat über 20 Jahre Erfahrung mit Java und ist bei der Mannheimer Firma OIO Orientation in Objects GmbH als Trainer, Software-Entwickler und Projektleiter tätig.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: