java 15 featuresjava 15 features
HappyCoders Glasses

Java 15 Features
(mit Beispielen)

Sven Woltmann
Sven Woltmann
22. November 2021

Java 15 brachte uns am 15. September 2020 mit "Text Blocks" die dritte Sprachverbesserung aus Project Amber (nach "var" in Java 10 und "Switch Expressions" in Java 14) – und mit ZGC und Shenandoah zwei neue, auf sehr kurze Pausen optimierte Garbage Collectoren.

Doch das ist längst nicht alles: Insgesamt 14 JDK Enhancement Proposals (JEPs) haben es in dieses Release geschafft.

Ich habe die Änderungen wie immer nach Relevanz für die tägliche Programmierarbeit sortiert. Nach den bereits genannten Features folgen Erweiterungen der JDK-Klassenbibliothek, Performance-Änderungen, experimentelle, Preview- und Incubator-Features, Deprecations und Löschungen und zuletzt sonstige Änderungen, mit denen wir eher selten in Berührung kommen.

Ich verwende wie immer die englischen Bezeichnungen der JEPs. Diese auf deutsch zu übersetzen würde keinen Mehrwert bringen.

Text Blocks

Wenn wir bisher in Java einen mehrzeiligen String definieren wollten, sah das in etwa so aus:

String sql =
    "  SELECT id, title, text\n"
        + "    FROM Article\n"
        + "   WHERE category = \"Java\"\n"
        + "ORDER BY title";Code-Sprache: Java (java)

Ab Java 15 können wir diesen String als "Text Block" notieren:

String sql = """
      SELECT id, title, text
        FROM Article
       WHERE category = "Java"
    ORDER BY title""";Code-Sprache: Java (java)

Wie genau man Text Blocks schreibt und formatiert, welche Escape-Sequenzen wir nicht mehr benötigen ... und welche wir stattdessen zur Verfügung haben, erfährst du im Hauptartikel "Java Text Blocks".

(Text Blocks wurden erstmal als Preview-Feature in Java 13 vorgestellt. Sie waren ein Ersatz für den von der Community nicht akzeptierten und daraufhin zurückgezogenen JEP 326, "Raw String Literals". Im zweiten Preview wurden in Java 14 zwei neue Escape-Sequenzen hinzugefügt. Aufgrund des positiven Feedbacks wurden Text Blocks in Java 15 mit dem JDK Enhancement Proposal 378 ohne weitere Änderungen als produktionsreifes Feature veröffentlicht.)

Neue Garbage Collectoren: ZGC + Shenandoah

Die Anforderungen an moderne Applikationen werden immer anspruchsvoller. Bei einem Speicherbedarf von Gigabytes bis hin zu Terabytes sollen sie mitunter Antwortzeiten im einstelligen Millisekundenbereich erreichen.

Herkömmliche Garbage Collectoren (wie der Allrounder G1) mit Stop-the-World-Phasen von hundert Millisekunden und mehr sind für solche Anforderungen nicht optimal geeignet.

Mit dem Ziel die Stop-the-World-Pausen so gut es geht zu eliminieren (in dem ein Großteil der Arbeit parallel zur laufenden Anwendung erledigt wird), oder wenigstens auf wenige Millisekunden zu reduzieren, haben Oracle und RedHat zwei neue Garbage Collectoren entwickelt, die seit Java 11 bzw. 12 als Preview-Features mit ausgeliefert wurden.

Ab Java 15 sind sie bereit für den produktiven Einsatz und machen die Java-Plattform hoffentlich für noch mehr Entwicklerinnen und Entwickler attraktiv.

ZGC: A Scalable Low-Latency Garbage Collector

Der Z Garbage Collector, kurz ZGC, verspricht Pausezeiten von 10 ms nicht zu überschreiten und dabei den Gesamtdurchsatz der Anwendung um nicht mehr als 15 % gegenüber dem G1GC zu reduzieren (die Verringerung des Durchsatzes sind die Kosten für die niedrige Latenz).

ZGC unterstützt Heap-Größen von 8 MB bis zu 16 TB.

Die Pausenzeiten sind dabei unabhängig sowohl von der Heap-Größe als auch von der Anzahl der überlebenden Objekte.

Genau wie G1, basiert ZGC auf Regionen, ist NUMA-kompatibel, und kann ungenutzten Speicher an das Betriebssystem zurückgeben.

ZGC lässt sich außerdem mit einer "weichen" Heap-Obergrenze konfigurieren (VM-Option -XX:SoftMaxHeapSize): ZGC wird diese Grenze nur dann überschreiten, wenn es zur Vermeidung eines OutOfMemoryErrors notwendig ist.

Aktiviert wird ZGC mit der folgenden VM-Option:

-XX:+UseZGC

Die detaillierte Funktionsweise von ZGC würde den Rahmen dieses Artikels sprengen. Du kannst sie im ZGC-Wiki nachlesen.

(Erstmals war ZGC als Preview in Java 11 enthalten. In Java 13 kamen die Uncommit- und SoftMaxHeapSize-Funktion hinzu. Seit Java 14 ist ZGC auch für Windows und macOS verfügbar. Mit dem JDK Enhancement Proposal 377 wurde ZGC in Java 15 für den produktiven Einsatz freigegeben.)

Shenandoah: A Low-Pause-Time Garbage Collector

Genau wie ZGC verspricht auch Shenandoah minimale Pausezeiten, unabhängig von der Heap-Größe.

Wie genau Shenandoah das erreicht, kannst du im Shenandoah-Wiki nachlesen.

Aktivieren kannst du Shenandoah mit der folgenden VM-Option:

-XX:+UseShenandoahGC

Genau wie G1 und ZGC, gibt auch Shenandoah ungenutzten Speicherplatz nach einer Weile wieder an das Betriebssystem zurück.

Support für NUMA und SoftMaxHeapSize gibt es aktuell nicht; zumindest NUMA-Support ist jedoch geplant.

(Shenandoah ist seit Java 12 als Preview im JDK enthalten. Mit dem JDK Enhancement Proposal 379 wurde Shenandoah für den produktiven Einsatz freigegeben.)

Neue String- und CharSequence-Methoden

Die Klassen String und CharSequence wurden in Java 15 um einige Methoden erweitert. Diese Erweiterungen sind nicht in JDK Enhancement Proposals definiert.

String.formatted()

Platzhalter in einem String konnten wir bisher beispielsweise wie folgt ersetzen:

String message =
    String.format(
        "User %,d with username %s logged in at %s.",
        userId, username, ZonedDateTime.now());Code-Sprache: Java (java)

Ab Java 15 können wir eine alternative Syntax verwenden:

String message =
    "User %,d with username %s logged in at %s."
        .formatted(userId, username, ZonedDateTime.now());Code-Sprache: Java (java)

Welche Methode du verwendest, macht keinen Unterschied. Beide Methoden rufen letztendlich den folgenden Code auf:

String message =
    new Formatter()
        .format(
            "User %,d with username %s logged in at %s.",
            userId, username, ZonedDateTime.now())
        .toString();Code-Sprache: Java (java)

Die Auswahl ist also letztlich Geschmacksache. Ich persönlich habe mich schnell mit der neuen Schreibweise angefreundet.

String.stripIndent()

Nehmen wir an, wir haben einen String, der in jeder Zeile eingerückt ist und der in jeder Zeile einige abschließende Leerzeichen hat, wie z. B. der folgende. Wir geben ihn – von zwei senkrechten Strichen begrenzt – zeilenweise aus.

String html = """
      <html>    \s
        <body>      \s
          <h1>Hello!</h1>
        </body>    \s
      </html>         \s\
    """;

html.lines()
    .map(line -> "|" + line + "|")
    .forEachOrdered(System.out::println);Code-Sprache: Java (java)

Wie du im ersten Kapitel gelernt hast, orientiert sich die Ausrichtung eines Textblocks in diesem Fall an den schließenden Anführungszeichen. Die Ausgabe sieht also so aus:

|  <html>     |
|    <body>       |
|      <h1>Hello!</h1>|
|    </body>     |
|  </html>          |Code-Sprache: Klartext (plaintext)

Mit der Methode stripIndent() können wir die Einrückung und die abschließenden Leerzeichen entfernen:

html.stripIndent()
    .lines()
    .map(line -> "|" + line + "|")
    .forEachOrdered(System.out::println);Code-Sprache: Java (java)

Die Ausgabe ist nun:

|<html>|
|  <body>|
|    <h1>Hello!</h1>|
|  </body>|
|</html>|Code-Sprache: Klartext (plaintext)

String.translateEscapes()

Gelegentlich bekommen wir es mit einem String zu tun, der escapete Escape-Sequenzen enthält, wie z. B. der folgende:

String s = "foo\\nbar\\tbuzz\\\\";

System.out.println(s);Code-Sprache: Java (java)

Die Ausgabe sieht wie folgt aus:

foo\nbar\tbuzz\\Code-Sprache: Klartext (plaintext)

Manchmal wollen wir aber die ausgewerteten Escape-Sequenzen darstellen, also einen Zeilenumbruch statt "\n", einen Tab statt "\t" und einen Backslash statt "\\".

Bisher mussten wir dafür auf Third-Party-Libraries wie z. B. Apache Commons Text zurückgreifen:

System.out.println(StringEscapeUtils.unescapeJava(s));Code-Sprache: Java (java)

Ab Java 15 können wir uns die zusätzliche Abhängigkeit sparen und die JDK-Methode String.translateEscapes() einsetzen:

System.out.println(s.translateEscapes());Code-Sprache: Java (java)

Die Ausgabe lautet nun:

foo
bar     buzz\Code-Sprache: Klartext (plaintext)

CharSequence.isEmpty()

Neu ist auch die Default-Methode isEmpty() im Interface CharSequence. Die Methode prüft einfach, ob die Länge der Zeichenfolge 0 ist:

default boolean isEmpty() {
  return this.length() == 0;
}Code-Sprache: Java (java)

Diese Methode ist damit automatisch in den Klassen Segment, StringBuffer und StringBuilder verfügbar.

String und CharBuffer, die ebenfalls CharSequence implementieren, haben jeweils eine eigene, optimierte implementierung von isEmpty(). Bei String z. B. ist der Aufruf von length() unnötig teuer, da für die Berechnung der Länge eines Strings seit Java 9 (JEP 254 "Compact Strings") auch dessen Encoding berücksichtigt werden muss.

Helpful NullPointerExceptions

Die in Java 14 eingeführten "Helpful NullPointerExceptions" sind ab Java 15 standardmäßig aktiviert.

"Helpful NullPointerExceptions" zeigen uns nicht mehr nur, in welcher Codezeile eine NullPointerException aufgetreten ist, sondern auch welche Variable (oder welcher Rückgabewert) in der entsprechenden Zeile null ist und welche Methode deshalb nicht aufgerufen werden konnte.

Ein Beispiel dazu findest du im oben verlinkten Artikel.

Performance-Änderungen

Dieses Kapitel hieß in den bisherigen Teilen der Serie "Performance-Verbesserungen". Die im ersten Abschnitt dieses Kapitels beschriebene Änderung kann allerdings zu einer spürbaren Leistungsverschlechterung führen.

Daher habe ich mich entschieden, die Änderung in diesem Kapitel aufzunehmen und nicht unter "Deprecations und Löschungen" – und das Kapitel entsprechend umzubenennen.

Disable and Deprecate Biased Locking

Diese Änderung erkläre ich am besten an einem Beispiel.

Der folgende JMH-Benchmark misst, wie lange es dauert einen Vector mit zehn Millionen Zahlen zu befüllen (du findest den Code in diesem GitHub-Repository):

@Benchmark
@BenchmarkMode(Mode.SampleTime)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
public void test(Blackhole blackhole) {
  Vector<Integer> vector = new Vector<>(10_000_000);
  for (int i = 0; i < 10_000_000; i++) {
    vector.add(i);
  }
  blackhole.consume(vector);
}Code-Sprache: Java (java)

Ich empfehle den Test mit der VM-Option -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC zu starten, um den Epsilon Garbage Collector einzusetzen, der seit Java 11 als experimenteller Garbage Collector Teil des JDK ist.

Der Epsilon GC führt keine Garbage Collection durch und ist damit sehr gut geeignet, um GC-Beeinflussungen bei Tests zu vermeiden.

Ich habe den Test auf meinem Dell XPS 15 mit einem Intel Core i7-10750H ausgeführt – zunächst mit Java 14. Du findest das vollständige Testergebnis in der Datei vector_results_java14.txt. Die relevanten zwei Zeilen des Ergebnisses sind die folgenden:

Benchmark                                         Mode  Cnt  Score   Error  Units
BiasedLockingVectorBenchmark.test               sample  148  0,071 ± 0,001   s/opCode-Sprache: Klartext (plaintext)

Unter Java 14 dauert es also im Mittel 71 Millisekunden, um einen Vector mit zehn Millionen Elementen zu befüllen.

Als nächstes habe ich den Test mit Java 15 ausgeführt. Das Testergebnis liegt in der Datei vector_results_java15.txt. Hier die relevanten Zeilen der Ausgabe:

Benchmark                                         Mode  Cnt  Score   Error  Units
BiasedLockingVectorBenchmark.test               sample   55  0,202 ± 0,004   s/opCode-Sprache: Klartext (plaintext)

Unter Java 15 benötigt die gleiche Operation 202 Millisekunden, also fast dreimal so lang!

Wie kommt es dazu?

Der Grund ist – wie die Überschrift des Abschnitts bereits verraten hat – die Deaktivierung von "Biased Locking".

Was ist Biased Locking?

Bei Biased Locking handelt es sich um eine Optimierung der Thread-Synchronisierung mit dem Ziel den Synchronisations-Overhead zu reduzieren, wenn derselbe Monitor immer wieder von demselben Thread erworben wird (d. h. wenn derselbe Thread immer wieder Code aufruft, der auf dem gleichen Objekt synchronisiert ist).

Im Beispiel oben bedeutet das, dass der vector-Monitor beim ersten Aufruf der add()-Methode auf den Thread ausgerichtet ("gebiased") wird, in dem die Test-Methode ausgeführt wird. Dadurch wird der Erwerb des Monitors in den folgenden 9.999.999 Aufrufen der add()-Methode beschleunigt.

Die genaue Funktionsweise ist kompliziert, wobei wir bei der nächsten Frage wären:

Warum wurde Biased Locking deaktiviert?

Von Biased Locking profitieren hauptsächlich alte Anwendungen, die Datenstrukturen wie Vector, Hashtable oder StringBuffer verwenden, bei denen jeder Zugriff synchronisiert ist.

Moderne Anwendungen verwenden in der Regel nicht-synchronisierte Datenstrukturen wie ArrayList, HashMap oder StringBuilder – und die für Multithreading optimierten Datenstrukturen im Paket java.util.concurrent.

Da der Code für Biased Locking extrem komplex ist und tief in den JVM-Code eingreift, erfordert er einen hohen Wartungsaufwand und macht Änderungen innerhalb des Synchronisationssystems der JVM aufwändig und fehleranfällig.

Daher wurde im JDK Enhancement Proposal 374 entschieden Biased Locking in Java 15 standardmäßig zu deaktivieren, als "deprecated" zu markieren und in einem der folgenden Releases vollständig zu entfernen.

Was bedeutet das für uns Java-Entwicklerinnen und -Entwickler?

Wenn nicht längst geschehen, ist es spätestens jetzt an der Zeit, Vector und Hashtable durch ArrayList und HashMap (oder andere geeignete Datenstrukturen) zu ersetzen.

Der Vollständigkeit halber hier noch ein Test für ArrayList (das vollständige Ergebnis findest du in der Datei arraylist_results.txt):

Benchmark                               Mode  Cnt  Score   Error  Units
ArrayListBenchmark.test               sample  160  0,064 ± 0,001   s/opCode-Sprache: Klartext (plaintext)

ArrayList ist also etwa 10 % schneller als Vector mit Biased Locking und mehr als dreimal so schnell wie Vector ohne Biased Locking.

Spezialisierte Implementierungen von TreeMap-Methoden

In TreeMap wurden spezialisierte Methoden putIfAbsent(), computeIfAbsent(), computeIfPresent(), compute() und merge() implementiert.

Diese Methoden waren seit Java 8 ausschließlich als Default-Methoden im Map-Interface spezifiziert.

Die TreeMap-spezifischen Implementierungen sind für den zugrunde liegenden Rot-Schwarz-Baum optimiert; sie sind dementsprechend performanter als die Default-Methoden des Interfaces.

(Die Erweiterung der TreeMap ist nicht in einem JDK Enhancement Proposal definiert.)

Experimentelle, Preview- und Incubator-Features

In Java 15 gibt es mit "Sealed Classes" ein neues Preview-Feature. Drei weitere Features wurden in die zweite Preview- bzw. Incubator-Runde geschicht.

Ich werde die neuen Features an dieser Stelle nicht in allen Details vorstellen, sondern auf das jeweilige Java-Release verweisen, in dem die Features Produktionsreife erreichen.

Sealed Classes (Preview)

Es gibt verschiedene Gründe die Vererbbarkeit einer Klasse einzuschränken (welche das sind, erfährst du im Hauptartikel über Sealed Classes).

Bisher gab es jedoch nur begrenzte Möglichkeiten die Vererbbarkeit einer Klasse einzuschränken:

  1. Die Klasse kann als final deklariert werden, so dass generell keine Unterklassen implementiert werden können.
  2. Die Klasse kann als package-private markiert werden und so nur Unterklassen innerhalb des Pakets erlauben. Dadurch ist die Oberklasse allerdings nicht mehr außerhalb des Pakets sichtbar, selbst wenn die abgeleiteten Klassen public gemacht werden. Das ist in den meisten Fällen unerwünscht.

Die durch JDK Enhancement Proposal 360 als Preview-Feature eingeführten "Sealed Classes" bieten Entwicklerinnen und Entwicklern einer Java-Klasse oder eines Interfaces die Möglichkeit einzuschränken, welche anderen Klassen und Interfaces sie erweitern bzw. implementieren dürfen.

Eine versiegelte Klassenstruktur wird wie folgt definiert:

  • Das Keyword sealed markiert eine versiegelte Klasse.
  • Mit dem Keyword permits werden die erlaubten Unterklassen aufgelistet.
  • Eine Unterklasse einer versiegelten Klasse muss entweder sealed, final oder non-sealed sein. Im ersten Fall müssen wiederum mit permits die erlaubten Unterklassen definiert werden. Der letzte Fall bedeutet, dass die Unterklasse wieder offen für Vererbung ist – genau wie eine reguläre Klasse.

Hier ein Beispiel:

public sealed class Shape permits Circle, Square, Rectangle, WeirdShape { ... }

public final class Circle extends Shape { ... }
public final class Square extends Shape { ... }

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }

public non-sealed class WeirdShape extends Shape { ... }Code-Sprache: Java (java)

Das folgende Klassendiagramm zeigt die im Beispielcode implementierte Klassenhierarchie. Die orangenen Rechtecke demonstrieren, dass die Hierarchie nur unter WeirdShape erweiterbar ist.

Klassenhierarchie mit "Sealed Classes"
Klassenhierarchie mit "Sealed Classes"

In Kombination mit "Pattern Matching for switch", was in Java 17 als Preview-Feature vorgestellt wird, werden versiegelte Klassen zudem eine Erschöpfungsanalyse ermöglichen (d. h. der Compiler kann prüfen, ob ein Switch-Ausdruck alle möglichen Klassen abdeckt). Mehr dazu im Hauptartikel über Sealed Classes.

Um Sealed Classes bereits in Java 15 einzusetzen, musst du sie entweder in deiner IDE aktivieren (in IntelliJ über File→Project Structure→Project Settings→Project→Project language level) oder mit der Option --enable-preview beim Aufruf der javac- und java-Kommandos.

Pattern Matching for instanceof (Second Preview)

"Pattern Matching for instanceof" wurde als Preview in Java 14 vorgestellt.

Mit dem JDK Enhancement Proposal 375 wird das Feature ohne Änderungen ein zweites Mal als Preview-Feature geliefert, um weiteres Feedback aus der Java-Community einzusammeln.

"Pattern Matching for instanceof" wird im kommenden Release, Java 16, produktionsreif sein.

Records (Second Preview)

Records wurden ebenfalls als Preview-Feature in Java 14 präsentiert.

Eine schnelle Wiederholung: Mit einem Record definieren wir eine Klasse mit nur finalen Feldern, wie in folgendem Beispiel:

record Point(int x, int y) {}Code-Sprache: Java (java)

Wir können einen Record wie folgt instanziieren und dessen Felder auslesen:

Point p = new Point(3, 5);
int x = p.x();
int y = p.y();Code-Sprache: Java (java)

Mit dem JDK Enhancement Proposal 384 wurde für Java 15 noch etwas Feinschliff vorgenommen:

  1. Felder eines Records können nicht mehr per Reflection verändert werden.
  2. Records können mit "Sealed Interfaces" kombiniert werden.
  3. Innerhalb von Methoden können "lokale Records" definiert werden.

Gehen wir die Änderungen im Einzelnen durch...

Felder eines Records per Reflection ändern

In Java 14 war es noch möglich, die eigentlich finalen Felder eines Records per Reflection zu ändern. Das folgende Beispiel zeigt, wie man den x-Wert des oben gezeigten Point-Records p nachträglich ändern konnte:

Field X = Point.class.getDeclaredField("x");
X.setAccessible(true);
X.set(p, newX);Code-Sprache: Java (java)

In Java 15 führt dieser Versuch zu einer IllegalAccessException.

Records und Sealed Interfaces

Records können die ebenfalls als Preview-Feature in Java 15 hinzugekommenen "Sealed Interfaces" implementieren. Entsprechend dürfen "Sealed Interfaces" auch Records in ihrer "permits"-Liste aufzählen.

Lokale Records

Records dürfen jetzt auch innerhalb von Methoden definiert werden und sind dann nur innerhalb dieser Methode sichtbar. Dies ist insbesondere nützlich, wenn man Zwischenergebnisse mit mehreren zusammengehörigen Variablen speichern möchte.

Ein Beispiel dafür findest du im Hauptartikel über Records.

(Records werden im nächsten Release, Java 16, als finale Version veröffentlicht werden. Eine Vorstellung in allen Einzelheiten findest du im oben verlinkten Artikel.)

Foreign-Memory Access API (Second Incubator)

Das ebenfalls in Java 14 als Incubator vorgestellte Foreign-Memory Access API erlaubt es Java-Anwendungen, effizient und sicher auf Speicher außerhalb des Java-Heaps zuzugreifen.

Mit dem JDK Enhancement Proposal 383 wurden einige Änderungen am API vorgenommen.

Diese Schnittstelle wird bis Java 18 im Incubator-Status bleiben und in Java 19 als Foreign Function & Memory API zum ersten Mal als Preview-Version erscheinen.

Deprecations und Löschungen

In diesem Abschnitt findest du Features, die in Java 15 als "deprecated" markiert oder vollständig aus dem JDK entfernt wurden.

Remove the Nashorn JavaScript Engine

Die im JDK 8 eingeführte und in Java 11 als "deprecated" markierte JavaScript-Engine "Nashorn" wird durch das JDK Enhancement Proposal 372 in Java 15 vollständig aus dem JDK entfernt.

Als Grund geben die JDK-Entwickler die rasante Entwicklungsgeschwindigkeit von ECMAScript (dem Standard hinter JavaScript) an. Diese macht die Weiterentwicklung von Nashorn zu einer nicht zu bewältigenden Herausforderung.

Remove the Solaris and SPARC Ports

Die Ports für das nicht ganz mehr zeitgemäße Betriebssystem Solaris und die SPARC-Prozessorarchitektur wurden in Java 14 als "deprecated" markiert.

Mit dem JDK Enhancement Proposal 381 werden die Portierungen Solaris/SPARC, Solaris/x64 und Linux/SPARC in Java 15 endgültig aus dem JDK entfernt, um die Entwicklungsresourcen für andere Projekte freizumachen.

Deprecate RMI Activation for Removal

Java Remote Method Invocation (Java RMI) ist eine Technologie, die Objekten einer JVM den Aufruf von Methoden auf Objekten einer anderen JVM ("entfernten Objekten") ermöglicht.

Ein faktisch nicht genutztes, aber aufwändig zu wartendes Feature von RMI ist RMI Activation.

Durch RMI Activation kann ein Objekt, das auf der Ziel-JVM zerstört wurde, bei einem RMI-Aufruf automatisch neu instanziiert werden. Dadurch soll eine komplexe Fehlerbehandlung im RMI-Client vermieden werden.

Es zeigt sich jedoch, dass die tatsächliche Nutzung von RMI Activation verschwindend gering ist. Die JDK-Entwickler haben dazu Open-Source-Projekte, Stack Overflow und andere Foren nach RMI Activation durchsucht und nahezu keine Erwähnung gefunden.

Die durch RMI Activation verursachten fortlaufenden Wartungskosten stehen somit in keinem Verhältnis zum Nutzen. RMI Activation wird daher durch JEP 385 als "deprecated for removal" markiert. Im übernächsten Release, Java 17, wird es komplett entfernt werden.

Sonstige Änderungen in Java 15

In diesem Kapitel haben ich Änderungen aufgelistet, die man als Java-Entwicklerin oder -Entwickler nicht zwingend kennen muss. Es schadet aber auch nicht, diesen Abschnitt einmal zu überfliegen :-)

Hidden Classes

Application Frameworks wie Java EE und Spring generieren zahlreiche Klassen dynamisch zur Laufzeit. Insbesondere erstellen sie Proxys für Anwendungsklassen, um Funktionen wie Zugriffskontrolle, Caching, Transaktionsmanagment und JPA Lazy Loading hinzuzufügen.

Die vorhandenen APIs ClassLoader.defineClass() und Lookup.defineClass() generieren Bytecode, der nicht von dem Bytecode zu unterscheiden ist, der beim Compilieren von statischen Anwendungsklassen entsteht.

Somit sind die dynamisch erzeugten Klassen für alle anderen Klassen der Class-Loader-Hierarchie auffindbar und existieren so lange wie der Class Loader, in dem sie generiert wurden.

Dies ist in der Regel unerwünscht. Zum einen gelten sie meist als Framework-spezifische Implementierungsdetails, die vor dem Rest der Anwendung verborgen bleiben sollen; zum anderen werden sie oft nur für eine bestimmte Zeit benötigt, so dass sie nach ihrer Nutzung den Speicherbedarf der Anwendung unnötig erhöhen.

Durch das JDK Enhancement Proposal 371 haben in Java 15 "Hidden Classes" Einzug ins JDK gehalten.

Hidden Classes werden über die Methode MethodHandles.Lookup.defineHiddenClass() definiert und können von anderen Klassen nicht verwendet werden – weder direkt, noch über Reflection.

Da das Feature von den meisten Java-Entwicklerinnen und -Entwicklern nicht direkt eingesetzt werden wird, verzichte ich hier auf tiefergehende Details.

Edwards-Curve Digital Signature Algorithm (EdDSA)

EdDSA ist ein modernes Signaturverfahren, das bei gleicher Sicherheitsstärke schneller ist als bisherige Signaturverfahren, wie DSA und ECDSA. EdDSA wird von vielen Kryptobibliotheken wie OpenSSL und BoringSSL unterstützt. Viele Benutzer verwenden bereits EdDSA-Zertifikate.

Durch das JDK Enhancement Proposal 339 erhält der EdDSA-Signaturalgorithmus Einzug in Java 15.

Das folgende Beispiel zeigt, wie du für die Nachricht "Happy Coding!" eine digitale Signatur erstellen kannst:

String message = "Happy Coding!";

KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair kp = kpg.generateKeyPair();

Signature sig = Signature.getInstance("Ed25519");
sig.initSign(kp.getPrivate());
sig.update(message.getBytes(StandardCharsets.UTF_8));
byte[] signature = sig.sign();

System.out.println("signature = " + Base64.getEncoder().encodeToString(signature));Code-Sprache: Java (java)

Wenn du das Programm unter einem älteren Release als Java 15 laufen lässt, bekommst du eine NoSuchAlgorithmException mit der Nachricht "Ed25519 KeyPairGenerator not available".

Reimplement the Legacy DatagramSocket API

Die in java.net.DatagramSocket und java.net.MulticastSocket implementierte "DatagramSocket API" existiert seit Java 1.0 und ist eine Mischung aus veraltetem, schwer wart- und erweiterbarem Java- und C-Code.

Inbesondere die IPv6-Unterstützung ist nicht sauber implementiert, und es existieren einige Concurrency Bugs, die sich ohne größere Refactoring nicht beheben lassen. Auch lässt sich der bestehenden Code nicht gut an leichtgewichtige Threads ("Fibers") anpassen, die in Projekt Loom entwickelt werden.

Durch den JDK Enhancement Proposal 373 wird die API durch eine einfachere, modernere, leichter wartbare und an leichtgewichtige Threads anpassbare Implementierung ersetzt.

Bereits in Java 13 wurde die ebenfalls aus Java 1.0 stammende "Socket API" neu implementiert.

Komprimierte Heap Dumps

Um die auf dem Heap liegenden Objekte einer laufenden Anwendung zu analyiseren, kann man wie folgt einen Heap-Dump erstellen:

jcmd <Prozess-ID> GC.heap_dump <Dateiname>Code-Sprache: Klartext (plaintext)

Je nach Art der Anwendung kann die generierte Datei mehrere GB groß werden.

Seit Java 15 gibt es die Option die Datei ZIP-komprimiert zu speichern. Dazu muss der Parameter -gz mit einem Wert von 1 (schnellste Kompression) bis 9 (beste Kompression) angegeben werden. Hier ein Beispiel:

jcmd 10664 GC.heap_dump /tmp/heap.dmp -gz=5Code-Sprache: Klartext (plaintext)

Basierend auf einigen Tests, würde ich in der Regel den Kompressionslevel 1 empfehlen. Dieser erreicht eine Reduktion des Dumps auf etwa 30 % seiner Originalgröße. Kompressionslevel 9 schafft 26 %, braucht dafür aber mehr als 20 mal so lange.

Support für Unicode 13.0

Eine Anhebung des Unicode-Supports begleitet uns in nahezu jedem Java-Release:

  • Java 11: Unicode 10
  • Java 12: Unicode 11
  • Java 13: Unicode 12.1

In Java 15 wird der Support nun auf Unicode 13.0 angehoben. Relevant ist das u. a. für die Klassen String und Character, die mit den neuen Zeichen, Codeblöcken und Schriftsystemen umgehen können müssen.

Ein Beispiel findest du im Artikel über Java 11.

(Für die Unterstützung von Unicode 13.0 existiert kein JDK Enhancement Proposal.)

Vollständige Liste aller Änderungen in Java 15

Dieser Artikel hat alle Features von Java 15 vorgestellt, die in JDK Enhancement Proposals definiert sind, sowie einige Performance-Verbesserungen und Löschungen, die keinem JEP zugeordnet sind.

Eine vollständige Liste aller Änderungen findest du in den offiziellen Java 15 Release Notes.

Fazit

Java 15 war wieder ein beeindruckendes Release:

  • Mit Text Blocks können wir endlich mehrzeilige Strings leserlich darstellen.
  • Die neuen Garbage Collectoren ZGC und Shenandoah können wir einsetzen, wenn wir die Pausezeiten unserer Applikation auf unter 10 ms reduzieren wollen.
  • String wurde um die Funktionen formatted(), stripIndent() und translateEscapes() erweitert.
  • Helpful NullPointerExceptions sind seit Java 15 standardmäßig aktiviert.
  • Durch die Deaktivierung von Biased Locking können Legacy-Anwendungen, die Datenstrukturen wie Vector oder Hashtable verwenden, spürbar langsamer werden.
  • Die TreeMap-Methoden putIfAbsent(), computeIfAbsent(), computeIfPresent(), compute() und merge() sind dafür schneller geworden.
  • Mit "Sealed Classes" wurde ein weiteres Feature aus Project Amber als Preview aufgenommen; für Records und "Pattern Matching for instanceof" gab es ein zweites Preview.
  • Entfernt wurde unter anderem die JavaScript Engine "Nashorn".

Wenn dir der Artikel gefallen hat, hinterlass mir gerne einen Kommentar oder teile den Artikel über einen der Share-Buttons am Ende.

Wenn du informiert werden möchtest, wenn der nächste Teil der Serie veröffentlicht wirst, klicke hier, um dich für den kostenlosen HappyCoders-Newsletter anzumelden.