Java 14 ist erschienen

An die halbjährlichen Updates der Programmiersprache Java haben wir uns gewöhnt, sie tun der Sprache und der Plattform gut. Zuletzt waren die Änderungen aber überschaubar gewesen. Mit dem Mitte März 2020 erschienen Java 14 gab es nun wieder ein regelrechtes Feuerwerk an neuen, spannenden Features. In diesem Artikel wollen wir die insbesondere aus Entwicklersicht relevanten Themen unter die Lupe nehmen. Schließlich steht im September mit Java 15 bereits die nächste Version vor der Tür.

Die folgenden Java Enhancement Proposals (JEP) wurden umgesetzt:

  • 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 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 zu den Enums. Entwickelt wurden Records im Rahmen des Projektes 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 zum Teil überflüssig machen. Kevlin Henney sieht außerdem noch folgenden Vorteil:

“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.”

Kevlin Henney

Die einfache Definition einer Person mit zwei Feldern sieht folgendermaßen aus:

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

Eine erweiterte Variante mit einem zusätzlichen Konstruktor, sodass 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 Zugriffsmethoden (allerdings keine Getter!), den Konstruktor, sowie equals/hashcode und toString enthält:

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 sieht erwartungsgemäß aus. Man sieht dem Aufrufer dabei nicht an, dass Record-Typen instanziiert werden:

var man = new Person("Adam");
var woman = new Person("Eve", man);
woman.toString(); // ==> "Person[name=Eve, partner=…]"

woman.partner().name(); // ==> "Adam"
woman.getNameInUppercase(); // ==> "EVE"

// Deep equals
// ==> true
new Person("Eve", new Person("Adam")).equals(woman); 

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 kann man im Übrigen auch mit Annotationen und JavaDocs erweitern. 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.

JEP 305: Pattern Matching for instanceof

Das Konzept des Pattern Matchings kommt bereits seit den 1960er Jahren bei diversen Programmiersprachen zum Einsatz. Zu den modernen Vertretern zählen Haskell und Scala. Ein Pattern ist eine Kombination aus einem Prädikat, welches 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 demnach die Destrukturierung von Objekten, also das Aufspalten in seine Bestandteile.

In Java kann man für solche Anwendungsfälle das Switch Statement benutzen, ist dabei aber auf die Datentypen Integer, String und Enum beschränkt. Bis es echtes Pattern Matching in Java gibt, wird es noch etwas dauern. Durch die Einführung der Switch Expression in Java 12 wurde aber bereits der erste Schritt dahin 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 folgendermaßen vorgehen:

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

Jetzt kann man beim Check mit instanceof den Wert direkt einer Variablen zuweisen und darauf weitere Operationen 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. Für die Puristen unter den Java-Entwicklern spart das allerdings eine kleine, aber dennoch lästige Redundanz ein.

Übrigens, die Switch Expression hatte man zunächst in Java 12 und 13 jeweils als Preview Feature eingeführt. Sie wurde nun im JEP 361 finalisiert. Dadurch stehen den Entwicklern zwei neue Syntaxvarianten mit einer kürzeren und klareren, sowie weniger fehleranfälligen Semantik zur Verfügung. Das Ergebnis der Expression kann einer Variablen zugewiesen oder als Wert aus einer Methode zurückgegeben werden. Weitere Details können dem Artikel zu Java 13 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 Null-Referenz ein Fehler mit Folgen in Höhe von vielen Milliarden Dollar. Und das nur, weil es bei der Entwicklung der Sprache Algol in den 1960er Jahren, einfach so leicht zu implementieren war.

In Java gibt es weder vom Compiler noch von der Laufzeitumgebung Unterstützung beim Umgang mit Null-Referenzen. Mit diversen Mustern und Vorgehensweisen lassen sich diese leidigen Exceptions allerdings 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 die API den Aufrufer darauf hinweisen, dass ein Wert null sein kann und er darauf reagieren muss. Somit kann man nicht mehr aus Versehen in eine Null-Referenz 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 die Syntax und den Compiler eingebaut, wie zum Beispiel in 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. Lange Rede, kurzer Sinn, mit den NullPointerExceptions werden wir auch zukünftig in Java 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 folgende Option aktivieren:

-XX:+ShowCodeDetailsInExceptionMessages

Wenn dann in einer Aufrufkette ein Wert null ist, 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 Lambda-Ausdrücken benötigt es eine Spezialbehandlung. Ist zum Beispiel der Parameter einer Lambda-Funkion null, bekommt man standardmäßig eine 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 "<parameter1>" is null

Damit der korrekte Parametername angezeigt wird, muss der Quellcode mit der Option -g:vars kompiliert werden. Das Resultat sieht dann folgendermaßen aus:

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

Wenn man aber wie hier im Beispiel jeden Stream-Methodenaufruf auf eine neue Zeile setzt, lässt sich die problematische Codezeile schnell eingrenzen.

Herausfordernd waren NullPointerExceptions bisher beim automatischen Boxing/Unboxing. Wird hier nun auch der Compiler Parameter -g:vars aktiviert, bekommt man ebenfalls die neue hilfreiche Fehlermeldung:

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 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:

// 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. Im nachfolgenden Beispiel bekommt man trotz Umbrüchen einen String mit einer langen Zeile:

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 zweite neue Escape-Sequenz ‘\s’ wird zu einem Leerzeichen übersetzt. Dadurch kann man erreichen, dass Leerzeichen am Zeilenende nicht automatisch abgeschnitten (getrimmt) werden und man beispielsweise eine feste Zeichenbreite je Zeile erhält:

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

Was gibt es noch Neues?

Neben den gerade beschriebenen Features, die hauptsächlich für Entwickler interessant sind, gibt es natürlich auch wieder diverse andere Änderungen. Im JEP 352 wurde die FileChannel API erweitert, um die Erzeugung von MappedByteBuffer Instanzen zu ermöglichen. Die arbeiten auf nichtflüchtigen Datenspeichern (NVM – non-volatile memory), im Gegensatz zum volatilen Speicher, dem RAM. Die Zielplattform ist allerdings auf Linux x64 beschränkt. Auch bei den Garbage Collectoren hat sich wieder etwas getan. So wurde der Concurrent Mark Sweep (CMS) Garbage Collector entfernt, und den ZGC gibt es 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 liest man die Daten dann mit dem Tool JDK Mission Control (JMC) aus, um sie zu analysieren. Neu im JDK 14 ist, dass man auch aus der Anwendung heraus asynchron die Events abfragen kann:

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, welches aber leider mitsamt JavaFX in den neueren Java Versionen 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. Der Inhalt ist die Java-Anwendung mitsamt einer Laufzeit-Umgebung. Das Tool baut aus diesem Input ein lauffähiges Binärartefakt, welches sämtliche Abhängigkeiten enthält (Formate: msi, exe, pkg als dmg, app als dmg, deb und rpm).

Ausblick

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. zu den Preview-Funktionen Feedback geben. Nur so ist sichergestellt, dass sie bis zur Finalisierung im nächsten LTS-Release, welches als Java 17 im Herbst 2021 erscheinen wird, gerüstet sind.

“Languages must evolve, or they risk becoming irrelevant.”, sagte Brian Goetz (Oracle) im November 2019 in seiner Präsentation “Java Language Futures” bei der Devoxx in Belgien. Er ist als Language Architect maßgeblich daran beteiligt, dass Java trotz seiner 25 Jahre noch lange nicht zum alten Eisen gehört. Oracle hat dazu in den vergangenen Jahren einige wegweisende Entscheidungen getroffen. Die halbjährlichen OpenJDK Releases mit den Preview Features wurden gut angenommen. Zudem hat Oracle sein Lizenzmodell geändert. Das Oracle JDK wird jetzt zwar nicht mehr kostenfrei angeboten, aber das hat den Wettbewerb angekurbelt. Und so bekommt man nun von diversen Anbietern, auch noch von Oracle, freie Distributionen des OpenJDK. Das ist seit Java 11 binärkompatibel zum Oracle JDK und steht unter einer OpenSource-Lizenz.

Wir können also festhalten, Java ist noch lange nicht tot. Außerdem sind aktuell noch viele Features in Arbeit, die in zukünftigen Versionen auf ihren Einsatz warten. Uns Java Entwicklern wird also nicht langweilig werden, die Zukunft sieht weiterhin rosig aus. Und im September 2020 erwartet uns bereits Java 15.

Die Code-Beispiele kommen zum größten Teil von meinem Kollegen Jonatan Kazmierczak. Vielen Dank dafür.

Short URL for this post: https://wp.me/p4nxik-3Dv
This entry was posted in Java Basics and tagged , , , . Bookmark the permalink.

Leave a Reply