Und halbjährlich grüßt das Murmeltier

Java 12: Alle neuen Features auf einen Blick

Falk Sippach

© Shutterstock/ Boontoom Sae-Kor

Java 7 ist noch gar nicht richtig kalt, Java 8 natürlich immer noch die am meisten genutzte Version, und nun kommt ein halbes Jahr nach dem JDK 11 (LTS-Release) bereits Java 12 heraus. So richtig haben wir Java-Entwickler uns noch nicht an diese kurzen Abstände gewöhnt. Andererseits ist es schön, schon wieder mit kleinen Sprachverbesserungen experimentieren zu können.

Java 12: Die neuen Features auf einen Blick

In Java 12  – bzw. in das JDK 12 – haben es die folgenden Java Enhancement Proposals (JEP) geschafft:

  • JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)
  • JEP 230: Microbenchmark Suite
  • JEP 325: Switch Expressions (Preview)
  • JEP 334: JVM Constants API
  • JEP 340: One AArch64 Port, Not Two
  • JEP 341: Default CDS Archives
  • JEP 344: Abortable Mixed Collections for G1
  • JEP 346: Promptly Return Unused Committed Memory from G1

Auf den ersten Blick sieht es nicht so viel aus, insbesondere, wenn man die langen Änderungslisten der letzten großen Releases im Hinterkopf hat, wie zum Beispiel die Lambdas und das Stream API in Java 8. Oder wenn man sich möglicherweise die Neuerungen im Fall des JDK 9 (Plattformmodulsystem, JShell, …) noch gar nicht näher angeschaut, geschweige denn verdaut hat.

Die Idee der halbjährlichen Releasezyklen ist es, dass neue Features parallel und ohne Zeitdruck, unabhängig von den nächsten Veröffentlichungsterminen entwickelt werden können. Die Aufnahme in das entsprechende Release erfolgt erst, wenn die Funktion ausgereift ist. Dadurch sind die Änderungen der einzelnen Releases natürlich überschaubar. Andererseits ist schon lange im Voraus klar, wann die nächste Version erscheinen wird; sie kann sich nicht mehr auf unbestimmte Zeit verzögern.

Switch Expressions in Java 12

Wenn man die obige Featureliste durchschaut, sticht aus Entwicklersicht hauptsächlich ein Punkt ins Auge: Switch Expressions. Nun ist das Konstrukt Switch eigentlich eine Idee aus der prozeduralen Programmierung und ein Überbleibsel aus der Tatsache, dass sich die Syntax von Java initial stark an C orientiert hat. Für den erfahrenen Programmierer ist das Switch Statement aber ein Code Smell und kann im Sinne der Objektorientierung besser mit dem Strategieentwurfsmuster ausgedrückt werden. Nichtsdestotrotz gibt es Situationen, in denen man mit einem Switch sehr effizient und übersichtlich Fallunterscheidungen umsetzen kann.

Allerdings hatte die Implementierung bisher einige Schwächen. So konnte man anfänglich nur auf Zahlen prüfen, ab Java 5 dann zwar auch auf Enums (verkappte Integer) und seit Java 7 immerhin auf Strings. In vielen Programmiersprachen gibt es dagegen keine Einschränkungen über die verwendbaren Datentypen. Zudem ist das in funktionalen Sprachen verbreitete Pattern Matching eine Art „Switch on Steroids“. Mit dem JEP 305 gibt es sogar Pläne, Pattern Matching in zukünftigen Java-Versionen einzuführen. Die Switch Expressions kann man schon mal als eine Art Vorgeschmack darauf verstehen. Nicht vergessen sollten wir aber, dass es sich aktuell in Java 12 nur um eine Preview handelt. Bis zum nächsten LTS-Release (JDK 17) kann sich durch Feedback aus der Community an der Umsetzung noch einmal einiges ändern oder möglicherweise auch wieder ganz verschwinden. Werfen wir in Listing 1 zunächst einen Blick auf das klassische Switch Statement.

public String describeInt(int i) {
  String str = "not set";
  switch(i) {
    case 1:
      str = "one";
    case 2: 
      str = "two";
      break;
    case 3:
      str = "three";
      break;
  }
  return str;
}

Probleme mit dem Switch Statement

Diese Variante wirkt etwas unhandlich und wird bei komplexeren case-Blöcken schnell unübersichtlich. Zudem können kleine Flüchtigkeitsfehler oder Unwissenheit über die genaue Funktionsweise zu schlecht nachvollziehbaren Bugs führen. Ein möglicherweise unabsichtlicher Fall Through entsteht, wenn man einen case-Block nicht mit einem break beendet. So springt die Ausführung weiter in den nächsten Block, auch wenn dessen Bedingung gar nicht zur Switch-Variable passt. In unserem Beispiel würde bei der Ausführung mit ì = 1 als Ergebnis two zurückgegeben. Sinnvoll kann dieses Verhalten wiederum sein, wenn mehrere Switch-Bedingungen auf das gleiche Ergebnis abgebildet werden sollen, wobei dann die durchfallenden case-Zweige explizit leer bleiben (Listing 2).

public String describeInt(int i) {
  String str = "not set";
  switch(i) {
    case 1:
    case 2: 
      str = "one or two";
      break;
    case 3:
      str = "three";
      break;
  }
  return str;
}

Sollte man vergessen, einen bestimmten Fall abzudecken, wird der Switch scheinbar nicht ausgeführt. Daher empfiehlt es sich immer, einen Defaultzweig zu definieren. Dieser kann dann wenigstens zur Laufzeit einen Fehler für nichtdefinierte Inputparameter werfen. Denn leider warnt uns der Compiler nicht, wenn wir den Switch mit Werten kleiner als 1 oder größer als 3 aufrufen.

In den unterschiedlichen case-Blöcken wird typischerweise redundant immer wieder die gleiche temporäre Variable mit natürlich unterschiedlichen Werten befüllt. Das widerspricht dem DRY-Prinzip und erschwert die Nachvollziehbarkeit, insbesondere wenn mit dem Fall Through gearbeitet wird. Temporäre Variablen sind nicht umsonst ein Code Smell. Idealerweise kann man komplett auf sie verzichten.

Zu guter Letzt führt der globale Scope des Switch Statements dazu, dass in den einzelnen Blöcken möglicherweise unterschiedliche Variablen definiert werden müssen, um ungewollte Nebeneffekte bei der Verwendung der gleichen Variablennamen zu vermeiden.

Switch in Java 12

Werfen wir nun einen Blick auf die Neuerungen des Previews in Java 12. So kann ein case-Block jetzt mehrere Labels haben und ermöglicht damit eine redundanzfreie und kompaktere Schreibweise, wenn wir einen Fall-Through verwenden (Listing 3).

public String describeInt(int i) {
  String str = "not set"; 
  switch (i) {
     case 1, 2: 
       str = "one or two";
       break;
     case 3: 
       str = "three";
       break; 
  }
  return str;
}

Mit der Pfeilsyntax existiert des Weiteren eine Variante, die der Definition von Lambdaausdrücken ähnelt. Jeder Zweig wird damit auf eine Zeile reduziert und das break entfällt. Das spart wieder Boiler-Plate-Code und macht Switch Statements kompakter und übersichtlicher. Ein unabsichtlicher Fall-Through ist nicht mehr möglich. Es wird immer nur genau das eine Statement auf der rechten Seite des Pfeils ausgeführt. Sobald mehr als ein Statement auf der rechten Seite benötigt wird, muss der Code innerhalb eines Blocks stehen. Damit wird ein eigener Scope geschaffen, der eine kollisionsfreie Variablendeklaration ermöglicht (Listing 4).

public String describeInt(int i) {
  String str = "not set"; 
  switch (i) {
    case 1, 2 -> str = "one or two";
    case 3 -> {
      var i = complexComputation();
      str = "three" + i;
    }
  }
  return str;
} 

In der funktionalen Programmierung gibt es typischerweise keine Zustandsänderungen in Form von Variablenzuweisungen. Vielmehr wird mit Ausdrücken gearbeitet, die wiederum als Inputparameter einer anderen Funktion zur weiteren Bearbeitung übergeben werden können. Die Switch Expression ist jetzt auch ein Ausdruck, der ein Ergebnis zurückliefert. So kann man das Resultat zum Beispiel direkt einer Variablen (besser Konstanten) zuweisen oder einem Methodenaufruf als Parameter über- bzw. per return zurückgeben (Listing 5).

public static String describeInt(int i) {
  return switch (i) {
    case 1, 2: 
      break "one or two";
    case 3: 
      break "three";
    default: 
      break "smaller than one or bigger than three";
  };
}

Jeder Zweig muss mit dem break jeweils genau einen Wert zurückliefern („Break with Value“-Semantik). Dadurch kann der Compiler prüfen, ob wir alle möglichen Fälle behandeln oder wenigstens einen Defaultblock als Fallback angegeben haben. Damit ist sichergestellt, dass immer genau ein Zweig abgearbeitet und dessen Ergebnis zurückgeliefert wird. Switch Statements ohne sichtbare Ergebnisse gehören somit der Vergangenheit an.

Durch die schon angesprochene alternative Pfeilnotation verkürzt sich die Syntax der Switch Expression nochmals (Listing 6).

public static String describeInt(int i) {
  return switch (i) {
    case 1, 2 -> "one or two";
    case 3 -> "three";
    default -> "smaller than one or more than three";
  };
} 

Die Switch Expression arbeitet natürlich auch nahtlos mit der in Java 10 eingeführten Local Variable Type Inference zusammen. Bei der Zuweisung zu einer mit var deklarierten Variable wird der spezifischste gemeinsame Typ aller Case-Zweige (kleinster gemeinsamer Nenner) ausgewählt.

Ein Nachteil von Switch Expressions ist, dass eine Expression letztendlich immer zu einem Wert aufgelöst werden muss. Dadurch ist es nicht möglich, innerhalb der Expression return oder continue zu benutzen. Das wurde bisher häufig verwendet, um direkt aus der Methode bzw. Iteration zu springen. Es ist allerdings weiterhin möglich, eine Exception zu werfen und damit die Expression frühzeitig zu beenden.

Bisher kann man in einem Switch die Datentypen byte, short, char, int und deren Wrapper-Klassen sowie Enums und Strings verwenden. Es gibt bereits Pläne, zusätzlich float, double und long zu unterstützen. Mit dem ebenfalls geplanten Pattern Matching kann man letztlich über beliebige Datentypen switchen. Die Idee des Pattern Matchings ist aber eine ganz andere, es wird zur Destrukturierung von Objekten eingesetzt.

Um die Switch Expressions bereits vor dem offiziellen Erscheinungstermin von Java 12 ausprobieren zu können, hatten manche IDEs (z. B. IntelliJ IDEA 2019.1) schon frühzeitig Support für das neue Previewfeature angeboten. Um es mit einer Early-Access-Version des JDK 12 testen zu können, musste man beim Starten des Java Interpreters zusätzlich die Parameter –enable-preview und –source 12 mitgeben:

java --enable-preview --source 12 SwitchExample.java

Ausblick Raw String Literals

Neben der Switch Expression gab es noch diverse weitere Neuerungen. So wird ab Java 12 eine Microbenchmark Suite ausgeliefert, die auf dem bekannten Java Microbenchmark Harness (JMH) basiert. Damit kann man nun mit JDK-Bordmitteln sehr einfach Benchmarks erstellen. Das JVM Constants API ermöglicht das typsichere und damit weniger fehleranfällige Laden von Werten aus dem Java Constants Pool (int, float, String, Class), ist aber eher ein JDK-internes Feature und wird vermutlich nur selten von einem Java-Programmierer benötigt werden.

Bei den Garbage Collectors gibt es ebenfalls wieder einige Neuerungen. So hat der Default-GC G1 Performanceverbesserungen erfahren. Mit Shenandoah wurde zudem ein neuer, noch experimenteller Garbage Collector eingeführt. Er wurde ursprünglich von Red Hat entwickelt und ist eine Alternative zum bereits existierenden ZGC, verwendet aber einen anderen Algorithmus. Dabei sind die STW-(Stop-the-World-)Pausen sehr kurz und zudem können große Mengen an Hauptspeicher (mehrere Terabyte Heap) effizient verwaltet werden. Dazu werden die Aufräumarbeiten konkurrierend zur eigentlichen Anwendung erledigt, was diese dann allerdings etwas langsamer macht. Um beim Starten von Java-Anwendungen nicht immer alle Metadaten der Klassen erneut laden zu müssen, werden diese Informationen in sogenannten Class-Data-Sharing-Archiven abgelegt, was die Startzeit verkürzt, die Performance der Garbage Collection verbessert und den Memory Footprint verringert.

In der JDK-Klassenbibliothek gibt es auch ein paar Änderungen. Die Klasse String hat zum Beispiel zwei neue Methoden spendiert bekommen, um entweder einen Text einzurücken (indent) oder umzuwandeln (transform). Und die Methode mismatch in Files findet das erste Byte, das sich zwischen zwei Dateien unterscheidet (Listing 8).

System.out.println("foobar".indent(5)); // '     foobar'

final Integer result = "42".transform(s -> Integer.valueOf(s));

final long mismatch = Files.mismatch(Path.of("a.out"), Path.of("b.out")); 

Ausblick Raw String Literals

Ursprünglich sollten neben den Switch Expressions auch die Raw String Literals in Java 12 erscheinen. Allerdings wurde die Veröffentlichung dieses Features auf unbestimmte Zeit verschoben, weil noch der nötige Feinschliff gefehlt hat. Werfen wir aber bereits jetzt einen kurzen Blick darauf, was uns in einem der nächsten Releases erwarten wird. Die Hauptprobleme in den klassischen Java-Strings sind der unintuitive Einsatz von Zeilenumbrüchen (1\n2\n3) und das schlecht lesbare Escapen des Backslashs, was insbesondere Windows-Dateipfade und reguläre Ausdrücke unnötig verkompliziert (c:\\tmp“ bzw. „\\d{2}\\.\\d{3}).

Raw-String-Literale interpretieren abgesehen von CRLF bzw. LF keinerlei Escape-Sequenzen. Als Trennzeichen fungiert der Backtick:

var string = `1. Zeile \d{2}\.\d{3}
2. Zeile: c:\windows`

Übrigens kann ein Raw String Literal auch von mehrfachen Backticks umschlossen sein („Enthält ` im Inhalt„) und ermöglicht damit auch die Verwendung von Backticks im Text, ohne sie zu escapen.

Fazit

Die Idee mit den halbjährlichen Major Releases und damit regelmäßigen, wenn auch kleinen Updates scheint aufzugehen. Oracle hat bisher pünktlich geliefert, und so können wir bereits auf die nächsten Neuerungen im Oktober 2019 gespannt sein. Nicht vergessen darf man, dass Java 12 seitens Oracle nur für ein halbes Jahr mit Updates unterstützt wird. Man hat also jetzt die Wahl und bleibt entweder bis 2021 beim JDK 8 oder dem letzten LTS-Release JDK 11. Alternativ kann man die halbjährlichen Releases mitgehen, muss zwar häufig migrieren, vermeidet aber die aufwendigen Big-Bang-Umstiege auf eine neue Java-Version nach vielen Jahren. Zudem erhält man dadurch mit dem Oracle OpenJDK auch weiterhin kostenlose Updates. Denn sowohl das Oracle JDK 8 als auch 11 sind mittlerweile in Produktion nur noch kostenpflichtig einsetzbar. Allerdings findet man auch von anderen Anbietern wie AdoptOpenJDK, Amazon Corretto, Azul, Red Hat, IBM usw. sowohl freie als auch kommerzielle Alternativen mit Long Term Support, die über mehrere Jahre regelmäßig Sicherheitspatches zur Verfügung stellen wollen.

Egal, welche Updatestrategie man zukünftig anwenden möchte, es empfiehlt sich, das JDK 12 bereits jetzt herunterzuladen und sich mit den Neuerungen, insbesondere den Switch Expressions, vertraut zu machen.

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

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: