Java 17: Sprach-Freatures der neuen Langzeit-Support-Version

Welche Java Features helfen uns künftig besseren Code zu schreiben?
In den Java Versionen 12 bis 17 gibt es spannende, neue Features - ich zeige euch diese hier:
  • Text Blöcke
  • Erweiterungen bei instanceof
  • Neuerungen bei switch
  • Records
  • Versiegelte Klassen
  • RandomGenerator

🎓 Auf Udemy findet ihr meinen kostenloses Online-Kurs zu diesem Blog-Artikel: 
Java 17 @ Udemy

Neue Java Versionen

Seit Java 9 gibt es halbjährlich ein neues Release. Java 11 ist aufgrund des verlängerten Supports bis September 2026 ein wichtiges Release. Daher wird Java 11 aktuell häufig im Berufsumfeld eingesetzt.
Seit September 2021 gibt es mit Java 17 ein neues Release mit verlängertem Support (LTS). Dieses wird Java 11 im Berufsumfeld mit der Zeit ablösen.

Hier zeige ich Features der Java Versionen 12-17, welche direkten Einfluss auf die Code-Qualität haben und somit zum Thema Clean Code passen.
Einen guten Überblick über Java Versionen, Erscheinungsdaten und Feature-Listen findet ihr bei Wikipedia: https://en.wikipedia.org/wiki/Java_version_history

Die Feature-Liste eines halbjährigen Java-Releases ist überschaubar - hier die entsprechenden Links für Java 17: 
https://openjdk.java.net/projects/jdk/17/  
https://www.oracle.com/java/technologies/javase/17-relnote-issues.html

Java 17 installieren und in IntelliJ einrichten

Das JDK 17 kann man z.B. hier bei Oracle downloaden:
Die Installation verlief in Windows problemlos. Folgt einfach den wenigen Schritten im Installer.

IntelliJ findet das neu installierte JDK automatisch, so dass ihr es in neuen Projekten direkt verwenden könnt. Um falsche Compile-Fehler zu vermeiden, stellt sicher, dass ihr keine ältere IntelliJ Version als 2021-2.2 verwendet.

Wenn ihr Preview Features von Java 17 ausprobieren wollt, müsst ihr den Sprach-Level Preview setzen. Dazu verwendet ihr einfach die Aktion im IntelliJ Popup-Menu des Preivew Feature Compile-Fehlers - siehe dazu den folgenden Screenshot.

Aktivierung von Java 17 Preview Features

Das war schon die komplette Einrichtung von Java 17 in IntelliJ.

Java 17 in Eclipse

Die Einrichtung von JDK 17 in Eclipse funktioniert analog:
  1. JDK 17 downloaden und installieren
  2. Eclipse auf den letzten Stand updaten, mindestens Version 2021-12
    (Eclipse Updates können je nach installierten Plugins sperrig sein - manchmal ist eine Neuinstallation in der aktuellen Version einfacher, siehe dazu eclipse-als-ide-fur-java-spring.html)
  3. Danach könnt ihr über die Projekt Properties Java 17 im Java Build Path einstellen oder die Konfiguration eures Build-Tools (Maven, pom.xml, <java.version>) entsprechend anpassen. 
Damit die neuen Java Sprach-Features keine Compile-Fehler verursachen, muss in den Projekt Properties der Support für die Vorschau-Features (Preview JEPs) aktiviert werden. Dazu folgendem Klickpfad folgen:
Rechts-Klick auf neues Java Projekt => Properties => Java Compiler
=> "Enable preview Features for Java 1X" markieren => Apply and Close
Danach könnt ihr die folgenden Code-Ausschnitte ausprobieren.

Text Blöcke

Text Blöcke wurden in Java 13 als Vorschau-Feature (Preview-Feature) eingeführt. Seit Java 15 sind sie ein reguläres Feature, siehe auch https://openjdk.java.net/jeps/378

Beim Text Block geht es, um einfaches Schreiben von Strings, die sich über mehrere Zeilen erstrecken. Anhand eines Beispiels sehr ihr es direkt:

String html = """
        <!DOCTYPE HTML>
        <html>
    <head>
        <meta charset="utf-8" />
<title>Text Block feature demo</title>
    </head>
    <body>
        <h1>%d items created with following names:</h1>
<h1>%s</h1>
    </body>
</html>
""";

Ein Text Block beginnt und endet mit drei doppelten Anführungszeichen hintereinander. Alles dazwischen gehört zum Textblock. Der String muss nicht mehr am Zeilenende enden.
Ein tolles Feature für strukturierte Texte wie JSON, XML oder html, die als Konstanten im String definiert sind.

Neue Methode für Strings: formatted

Seit Java 15 haben Strings eine neue Methode formatted. Funktionell entspricht sie der statischen Methode String.format, kann aber direkt auf Strings angewendet werden.
Wenn wir zum Beispiel die beiden Platzhalter %d und %s im zuvor gezeigten Textblock ersetzen wollen, dann können wir das so tun:

String htmlWithReplacements = html.formatted(2, "Ball & Goal");

Vor Java 15 war es umständlicher und sah so aus (funktioniert immer noch):

String htmlWithReplacements = String.format(html, 2, "Ball & Goal");

Erweiterungen bei instanceof

Seit Java 14 gibt das Vorschau-Feature "Pattern Matching for instanceof". Seit Java 16 ist es ein reguläres Feature, siehe https://openjdk.java.net/jeps/394

Mit instanceof könnt ihr bisher prüfen, ob ein beliebiges Objekt Instanz einer bestimmten Klasse ist. Danach passierte typischerweise der Cast des Objektes, um es mit den Methoden der Klasse bearbeiten zu können. Das sah dann z. B. so aus:

List<Object> items = ...; // Get list somehow.
for (Object unknownType : items) {
    if (unknownType instanceof ItemAsRecord) {
        ItemAsRecord item = (ItemAsRecord) unknownType;
itemNames += item.name();
    }
} 

Dank der Erweiterungen bei instanceof können wir die Typprüfung (unknownType instanceof ItemAsRecord), die Definition der Variablen (ItemAsRecord item = ...) und das Casten ((ItemAsRecord) unknownType) wie folgt in einer Zeile machen.

    if (unknownType instanceof ItemAsRecord item) {
itemNames += item.name();
    }

Diese sinnvolle Erweiterung von instanceof reduziert das Schreiben von Boilerplate-Code. Die neu definierte Variable kann übrigens direkt im if Ausdruck verwendet werden:

    if (unknownType instanceof ItemAsRecord item && item.id() > 0)...

Neuerungen beim switch

Seit Java 12 gibt es Neuerungen am switch Befehl, die mit Java 14 den Vorschau-Status verlassen haben, siehe auch: https://openjdk.java.net/jeps/361
  • Es gibt nun eine kompaktere Schreibweise für switch Befehle, in der Cases zusammengefasst werden können, wenn in jedem Case das Gleiche getan werden soll.
  • Außerdem ist break am Ende jedes case nicht mehr nötig, da die neue switch Form am Ende des case-Blocks automatisch verlassen wird.
  • switch Ausdrücke können ein Ergebnis haben, welches direkt einer Variablen zugeordnet werden kann.
Hier ein Beispiel des neuen switch Befehls mit den 3 Neuerungen:

ItemAsRecord result = switch (type) {
    case "a", "A", "b", "B" -> this.typeB;
    case "c", "C" -> {
        logger.info("Type C is handled like default case.");
yield this.typeC;
    }
    default -> this.typeC;
};
  • Die Pfeile (->) drücken aus, dass nur der rechte Teil ausgeführt wird und machen daher  break überflüssig. Den Doppelpunkt (:), aus der bisherigen Form von switch Befehlen, gibt es immer noch und der Doppelpunkt benötigt auch immer noch ein break
  • yield ist ein neuer Ausdruck und definiert das Ergebnis des switch Befehls, welches im "c" oder "C" case der Variablen result zugewiesen wird. Im "a", "A", "b" oder "B" case ist yield nicht nötig, da sich dieser case vergleichbar zu einem Lambda Ausdruck verhält und den Wert rechts des Pfeils direkt zurückgibt.
  • Die Attribute this.typeB und this.typeC wurden vorher definiert und haben die Klasse ItemAsRecord, wobei das nichts mit den Neuerungen des switch Befehls zu tun hat. 
In der alten Form wäre dieses Beispiel viel umständlicher zu Schreiben gewesen - daher finde ich diese Neuerungen wirklich gut, sie werden uns helfen kompakteren und besseren Code zu schreiben. Hier noch die alte (von mir optimierte) Form zum Vergleich:

ItemAsRecord result;
switch (type) {
    case "a":
    case "A":
    case "b":
    case "B":
        result = this.typeB;
break;
    case "c":
    case "C":
        logger.info("Type C is handled as default case.");
    default:
        result = this.typeC;
}

Pattern Matching für switch

Mit Java 17 gibt es noch eine weitere Neuerung beim switch: Pattern Matching, siehe dazu auch https://openjdk.java.net/jeps/406. Das Pattern Matching bei switch funktioniert analog zum Pattern Matching bei instanceof. Das case Label ist keine einfache Konstante mehr, sondern kann nun sowohl Casten als auch Ausdrücke auswerten. null als case Label ist auch möglich. Dieses Feature ist allerdings noch im Preview Status, hier ein Beispiel:

BaseStream stream = getItFromSomewhere();
switch (stream) {
    case null -> log.info("null is now a possible case.");
    case IntStream is && is.isParallel() -> 
            log.info("Expression in case.");
    case DoubleStream ds -> log.info("Casted in case.");
    default -> throw new IllegalStateException();
}

Records

Records sind Klassen mit einer neuen kompakten Schreibweise zum Halten von unveränderlichen Daten. Seit Java Version 14 gibt es sie im Status Vorschau und mit Java 16 sind sie ein reguläres Feature, siehe auch https://openjdk.java.net/jeps/395. Ein Record sieht zum Beispiel so aus:

public record ItemAsRecord(
int id, 
String name, 
String location) {}

Der Record ist deutlich kompakter im Vergleich zur Definition als Klasse:

public class Item {
private final int id;
private final String name; 
private final String location;

public Item(int id, String name, String location) {
this.id = id;
this.name = name;
this.location = location;
}
// Getter Methoden ...
// equals Methode ...
// hashCode Methode ...
// toString Methode ...
}

Neben der Kompaktheit hat der Record den Vorteil, dass automatisch korrekte Implementierungen von den Methoden equals, hashCode und toString bereitgestellt werden. Getter-Methoden bestehen nur aus dem Attributnamen also name() statt getName(). Setter-Methoden gibt es nicht, da Records unveränderlich sind. Instanziert wird ein neuer Record immer mit dem Konstruktor, der alle Argumente enthält. Dieser Konstruktor ergibt sich bereits aus der Record Definition.

Da ich mich in anderen Blog-Artikeln mit Spring, RestController und Validierung befasse, war für mich eine interessante Fragestellung, ob Records mittels Jackson direkt geparst (rest-json-apis.html) und gleichzeitig per Annotation validiert werden können (siehe auch validation-with-spring.html)?
Ja, es ist möglich 😃 und sieht dann so aus:  

public record ItemAsRecord(
@Positive @JsonProperty("id") int id, 
@NotBlank @JsonProperty("name") String name, 
@Size(max = 5) @JsonProperty("location") String location){}

  • Mit @JsonProperty werden Mappings von JSON Attributen auf alle Record Attribute gemacht. Das kann man (hier) auch weglassen, weil die Namen identisch sind.
  • Die Validierungsannotationen (hier @Positive@NotBlank und @Size) setzt man einfach vor das Attribut, sie werden dann beim Record Konstruktor Aufruf geprüft.
  • Am RestController gibt es keine Besonderheiten, hier funktioniert alles so wie es auch mit herkömmlichen Klassen funktioniert. In GitHub findet ihr den kompletten Code, dort zeige ich auch den RestController, siehe https://github.com/elmar-brauch/java17.

Versiegelte Klassen

Seit Java 15 gibt es die Möglichkeit mit dem Schlüsselwort sealed Klassen und Interfaces zu versiegeln. Mit Java 17 wurde der Preview-Status verlassen, so dass versiegelte Klassen nun fester Bestandteil der Sprache sind, siehe auch https://openjdk.java.net/jeps/409.

Eine versiegelte Klasse bzw. ein versiegeltes Interface kann nur von, mittels Schlüsselwort permits, festgelegten Klassen erweitert (extends) bzw. implementiert (implements) werden. Das sieht dann zum Beispiel so aus:

public sealed abstract class ItemController 
        permits PostItemController, GetItemController {...} 

public final class GetItemController extends ItemController {...}

public final class PostItemController extends ItemController {...}

  • Die abstrakte Klasse ItemController ist mittels sealed als versiegelte Klasse definiert.
    Mit dem Schlüsselwort permits werden alle Klassen definiert, welche die abstrakte Klasse ItemController erweitern dürfen - das sind PostItemController und GetItemController in diesem Beispiel.
    Um ehrlich zu sein, das ist ein schlechtes Beispiel für eine versiegelte Klasse. Im JDK Enhancement-Proposal 360 (siehe https://openjdk.java.net/jeps/360) sind bessere Beispiele genannt.
  • Klassen, die eine versiegelte Klasse erweitern, müssen ebenfalls eine Modifizierung haben, möglich sind: sealed, non-sealed und final.
    • final kann als stärkste Form der Versieglung verstanden werden, da nun erweiternde oder implementierende Klassen nicht erlaubt sind.
    • non-sealed bricht die Versiegelung und die entsprechend modifizierte Klasse oder Interface ist wieder offen für Erweiterung oder Implementierung durch beliebige unbekannte Klassen.
Die Versiegelung ist sicherlich ein hilfreiches Mittel, um ein durch Fachlichkeit festgelegtes Modell als unveränderlich zu definieren. Wenn ich z.B. ein Modell der Euro-Münzen habe, kann ich mittels Versiegelung verhindern, das ein Entwickler eine 3 Euro Münze definiert.

In Bibliotheken, die als SDK fungieren und die Client-Implementierung zu einem Service bereitstellen, macht es bestimmt auch Sinn das Datenmodell für die Kommunikation zwischen Client und Server zu versiegeln. Dadurch könnte verhindert werden, dass durch Client-seitige Erweiterung des Datenmodell ungültige Daten zum Server geschickt werden.

RandomGenerator

Mit Java 17 gibt es mehr und verbesserte Algorithmen zur Generierung von Pseudo-Zufallszahlen, die Teil des JDKs sind. In der ersten Java Version gab es dazu die Klasse Random. Mit Java 17 gibt es RandomGenerator bzw. mehrere Implementierungen dieses Interfaces. Hier ein kleiner Code Ausschnitt als Demo: 

JumpableGenerator random =
    RandomGenerator.JumpableGenerator.of("Xoroshiro128PlusPlus");
int i = random.nextInt();
IntStream is = random.ints(5,1,100);
random.jump();
DoubleStream ds = random.doubles(5);
  • JumpableGenerator erweitert das Interface RandomGenerator. Mit Factory-Methode of wird eine neue Instanz des Pseudo Zufallszahlen Algorithmus "Xoroshiro128PlusPlus" erzeugt. Hier gibt es mit im neuen JDK noch eine längere Liste mit alternativen Algorithmen. Ich habe diesen per Zufall ausgewählt 😄
  • Die nextInt Methode liefert eine beliebige Integer Zufallszahl.
  • Methode ints liefert einen IntStream. Aufgrund der übergebenen Parametern enthält dieser Stream 5 Integers mit jeweils Werten zwischen 1 und 100.
  • Die Methode jump verändert den Zustand der JumpableGenerator Instanz, um auf Grund des neuen Zustands künftig andere Zufallszahlen zu generieren.
  • doubles(5) liefert einen DoubleStream mit 5 beliebigen Double Werten.
Alle Details zu den neuen, erweiterten, Pseudo-Zufallszahlen Generatoren findet ihr hier: https://openjdk.java.net/jeps/356

Verbesserte Performanz

Oracle berichtet in vielen Präsentationen zu neuen Java Versionen von Performanz Verbesserungen. Ein Java Update ist damit auch meist eine Chance mit wenig Programmieraufwand die eigene Anwendung zu tunen. Je nach Quelle und Benchmark Testverfahren werden unterschiedliche Leistungssteigerungen gemessen. Laut OptaPlanner ist Java 17 um 8,66% schneller als Java 11.

Fazit

Neben den vorgestellten Java Neuerungen gibt es noch weitere neue Funktionen in den Java Versionen 12 bis 17. Dazu ist die Wikipedia Seite eine gute Zusammenfassung:
https://en.wikipedia.org/wiki/Java_version_history

Ich habe Features präsentiert, die uns helfen Clean Code Prinzipien anzuwenden. Performanz-Steigerung, Garbage Collector Anpassungen oder neue Algorithmen für digitale Signaturen können andere wichtige Argumente für einen zeitnahen Umstieg auf Java 17 sein. Schaut euch die neue Java Version selbst an und hinterlasst einen Kommentar, welche Features aus eurer Sicht am wichtigsten sind.

Den kompletten Code zu meinem Artikel findet ihr in GitHub:
https://github.com/elmar-brauch/java17

Kommentare

Martin Both hat gesagt…
Ein Artikel über den Umgang mit null-Values fehlt noch. Das ist ein sehr wichtiges Thema. Optional<> als Return-Type und wie man mit diesen Return-Types elegant umgeht. Optional hat viele interessante Methoden, die aber aus Unkenntnis oft falsch eingesetzt werden.
Objects.requireNonNull() für Argumente wird zu wenig genutzt und Objects.equals() wird ebenfalls zu wenig genutzt, obwohl es hilft, die NullPointerExceptions beim Vergleich eines null-Values zu vermeiden. (value.equals("test") ist gefährlich, "test".equals(value) wird oft vergessen, Objects.equals(value, "test") ist einfach immer richtig).
Danke für den Kommentar zum Umgang mit null-Values.
Ich schau mal, ob ich dazu einen künftigen Blog-Artikel schreiben kann.
Mich haben Fragen zum Artikel erreicht, die ich hier für alle beantworten möchte:
- Gibt es Vererbung bei Records?
Nein, Extension ist nicht vorgesehen, records sind final, siehe dazu:
https://openjdk.java.net/jeps/395
- Unterstützt MapStruct Records?
Ja, seit Version 1.4.0, siehe:
https://mapstruct.org/news/2020-09-28-mapstruct-1_4_0_Final-is-out/

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux