Java 16 – ein Ausblick

19.02.2021

Gestern, am 18.02.2021, erschien der „Final Release Candidate“ für Java 16, das in knapp einem Monat veröffentlicht wird. Höchste Zeit also, einen Blick auf ein paar der Features zu werfen, die die neue Version mit sich bringt.

Migration zu Git und GitHub

Java 16 ist das erste Release, das mit dem Sourceverwaltungssystem Git auf Github entwickelt wurde. Im September 2020, zum Ende der Entwicklungsphase von Java 15, wurde die Migration von Mercurial zu Git vollzogen. Die Gründe für diese Entscheidung waren vornehmlich

  • dass Git das am weitesten verbreitete Codeverwaltungssystem ist und das beste Tooling bietet (z.B. für Editoren und IDEs)
  • der geringere Footprint der Repository-Metadaten: während ein geklontes JDK in Mercurial über ein GB Metadaten im Verzeichnis .hg auf der Platte belegt, sind es im .git-Verzeichnis nur ungefähr 300 MB.
  • Es existieren viele Services zum Hosten von Git-Repositories

Als Hoster entschied sich die JDK-Community für GitHub. Durch die Unterstützung von Pull Requests verspricht man sich Verbesserungen im Entwicklungsworkflow (bisher erfolgte die Zusammenarbeit mit Hilfe von Mailing Lists; diese soll zwar erhalten, aber durch die Möglichkeiten von GitHub ergänzt werden). Auch die bereits vorhandene große Nutzerbasis bei GitHub war ein Entscheidungskriterium. Für potenzielle neue Mitentwickler am JDK wird die Einstiegshürde so gering gehalten, da diese in der Regel bereits einen GitHub-Account besitzen und mit Pull Requests vertraut sind.

JEP 357: Migrate from Mercurial to Git

JEP 369: Migrate to GitHub

Records

Records sind seit Java 14 als Preview Feature vorhanden. Mit Java 16 halten sie nun offiziell Einzug in die Sprache.

Oftmals benötigt man ein Konstrukt dessen einziger Zweck es ist, Daten zu speichern. Man kenntdas vielleicht bereits aus Scala (case class) oder Kotlin (data class). In Java war es bisher nötig, dafür  Klassen zu schreiben und diese mit Konstruktor, Getter- und ggf. Setter-Methoden, equals()- und hashCode()- sowie toString()-Implementierungen auszustatten – oder sich mit einer Bibliothek wie z.B. Lombok zu behelfen. Eine Klasse, die einen Punkt in einem zweidimensionalen Koordinatensystem repräsentiert, sah bisher so aus (hier als „immutable“, d.h. unveränderbar implementiert):

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        // ...
    }

    @Override
    public int hashCode() {
        // ...
    }

    @Override
    public String toString() {
        // ...
    }
}

Mit Records erreicht man nun dasselbe mit nur einer Zeile Code:

record Point(int x, int y) { }

Ein paar Unterschiede zur herkömmlichen Klasse gibt es dann allerdings doch:

  • Record-Instanzen sind immer immutable.
  • Record-Klassen sind implizit „final“, man kann also keine Unterklassen von ihnen ableiten.
  • Die Getter haben kein „get“ im Methodennamen, und heißen entsprechend z.B. x() statt getX().

Die Vorteile von Records sind u.a.:

  • Kurzer, prägnanter Code
  • Aus der Syntax ist sofort ersichtlich dass es sich um reine Datenhaltungsklassen ohne weitere Funktionalität handelt
  • Geringere Fehleranfälligkeit, da man bei der Implementierung von equals() und hashCode() nichts falsch machen kann (bzw. vergessen diese anzupassen, wenn ein Datenattribut hinzugefügt wird)
  • Einfacheres Debuggen und Loggen, da toString() immer implementiert ist
  • Records funktionieren ohne weiteres Zutun wie erwartet, wenn sie als Keys in HashMaps oder in HashSets gespeichert werden
  • Mit Records werden die Voraussetzungen für Dekonstruktion sowie mächtiges Pattern Matching geschaffen, das in späteren Java-Versionen kommen soll

JEP 395: Records

Pattern Matching für instanceof

Auch dieses Feature existiert seit Java 14 als Preview, und wird in Java 16 nun finalisiert.

Prüft man ein Objekt mit instanceof, ob es von einem bestimmten Typ ist, sieht das bisher so aus:

CharSequence cs = new StringBuilder("61 avaJ olleH");
if(cs instanceof StringBuilder) {
    StringBuilder sb = (StringBuilder) cs;
    sb.reverse();
}

Ab Java 16 ist kein expliziter Cast mehr nötig. Stattdessen ist folgendes möglich:

CharSequence cs = new StringBuilder("61 avaJ olleH");
if(cs instanceof StringBuilder sb) {
    sb.reverse();
}

Dies ist die erste Stufe von Pattern Matching, das in späteren Java-Versionen ausgebaut werden soll.

JEP 394: Pattern Matching for instanceof

Warnungen für Value-Based Classes

In Java gibt es Klassen die lediglich einen Wert repräsentieren („value-based“), wie z.B. java.lang.Integer. Diese sind u.a. nötig um Zahlen in Datenstrukturen wie Listen, Maps oder Sets zu speichern. Gegenüber den primitiven Datentype wie etwa int bergen sie jedoch einen Ressourcen- und Performance-Overhead. Das effiziente Rechnen mit Konstrukten wie z.B. komplexen Zahlen ist deswegen bisher nicht möglich.

Zudem besitzen Instanzen solcher Klassen – wie jedes Java-Objekt – eine Identität, also ein Merkmal das zwei Objekte voneinander unterscheidet. Diese ist mitunter dafür verantwortlich, dass der  Vergleich zweier Integer-Instanzen per ==, die denselben Wert haben, nicht immer true ergibt (wenn es sich um zwei physisch unterschiedliche Objekte an verschiedenen Speicherorten handelt).

Bei wertbasierten Klassen ist dies jedoch nicht unbedingt sinnvoll und ggf. auch fehleranfällig. Aus diesem Grund sollen im Rahmen von Projekt Valhalla sogenannte Value Types eingeführt werden, die keine Identität haben und ähnlich speicher- und recheneffizient sein sollen wie primitive Datentypen.

Die Wrapperklassen für primitive Datentypen wie z.B. Integer, sollen zu Value Types werden. Dazu wurden deren Konstruktoren, die bereits seit Java 9 als „Deprecated“ markiert sind, nun zusätzlich mit „for removal“ gekennzeichnet, was bei deren Verwendung entsprechende Compiler-Warnungen zur Folge hat. Denn per Konstruktor erzeugte Objekte sind immer nicht-identisch zu allen anderen Objekten.

Außerdem wurden alle Kandidaten für künftige Value Types, darunter die Wrapperklassen, aber auch Zoned- und LocalDate(Time) u.v.m. mit der Annotation @jdk.internal.ValueBased versehen.

Anhand dieser Annotation können der Compiler sowie die Hotspot-VM wertbasierte Klassen identifizieren und entsprechende Warnungen ausgeben, wenn Instanzen dieser Klassen zur Synchronisierung von Threads verwendet werden:

Integer i = Integer.valueOf(42);
synchronized (i) { ... } // Compiler- u. Hotspot-Warnung

Object o = i;
synchronized (o) { ... } // Hotspot-Warnung

Da die Objektidentität ausschlaggebend für die Synchronisierung ist, soll dies mit Instanzen wertbasierter Klassen künftig nicht mehr funktionieren.

JEP 390: Warnings for Value-Based Classes

Sealed Classes (2. Preview)

Sealed Classes & Interfaces sind als 2. Preview in Java 16 enthalten. Entwickler können damit einschränken, welche Klassen von einer Oberklasse erben bzw. ein Interface implementieren dürfen.

Bei der Implementierung eines Zustandsautomaten gibt es beispielsweise eine fest definierte Anzahl an Zuständen. Mithilfe der „sealed“- und „permits“-Anweisungen kann dies wie folgt modelliert werden:

public abstract sealed class State permits On, Off, Standby {
    // ...
}

final class On extends State { ... };
final class Off extends State { ... };
final class Standby extends State { ... };

So wird sichergestellt, dass es außer On, Off und Standby keine weiteren Unterklassen von State gibt. Die Unterklassen müssen entweder final sein, so dass von ihnen keine weiteren Unterklassen abgeleitet werden können, oder selbst wiederum „sealed“ sein und mittels „permits“ angeben, welche Unterklassen von ihnen existieren dürfen.

Dasselbe ist auch mit Interfaces möglich:

public sealed interface State permits On, Off, Standby {
    // ...
}

final class On implements State { ... };
final class Off implements State { ... };
final class Standby implements State { ... };

Die Vorteile von Sealed Classes und Interfaces:

  • Die Information aus der Fachdomäne, dass es nur die drei erlaubten Zustände geben kann, wird explizit im Code ausgedrückt.
  • Der Compiler kennt alle Unterklassen einer sealed class, was z.B. künftig Pattern Matching in einem switch-Ausdruck ermöglicht:
    private static void turnOff(State state) {
        return switch (state) {
            case On on      -> new Off();
            case Off off    -> off;
            case Standby sb -> new Off();
            // no default necessary
        }
    }

JEP 397: Sealed Classes (Second Preview)

Neue Stream.toList()-Methode

Die am häufigsten terminierende Operation von java.util.stream.Stream  ist das Sammeln der Ergebnisobjekte in einer Liste. Dafür benötigte man bisher die Methode .collect(Collectors.toList()).

Ab Java 16 gibt es dafür nun auch die Methode .toList():

List<String> greeting = Stream.of("hello", "java", "16")
                .map(String::toUpperCase)
                .toList();

Aber Vorsicht: die Methode gibt eine unveränderbare („unmodifiable“) Liste zurück, im Gegensatz zu Collectors.toList(), die eine modifizierbare java.util.ArrayList zurückliefert. Warum das so ist, ist eine längere Geschichte und einen eigenen Blogbeitrag wert (coming soon ;-).

RFR: 8180352: Add Stream.toList() method

Was noch?

Neben den beschriebenen Features hat sich auch wieder viel unter der Haube getan. Der gesamte Umfang des Releases ist hier zu finden:

https://openjdk.java.net/projects/jdk/16/

Am 16. März erscheint Java 16, von da an dauert es nur noch ein halbes Jahr zur nächsten Version LTS-Version Java 17.

 

Mehr zu Java Programmierungen erfahren

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*