Java 12 Features mit Beispielen

Java 12 Features (mit Beispielen)

von Sven Woltmann – 1. November 2021

Artikelserie: Java-Versionen

Teil 1: Neue Features in Java 10

Teil 2: Neue Features in Java 11

Teil 3: Neue Features in Java 12

Teil 4: Neue Features in Java 13

Teil 5: Neue Features in Java 14

Teil 6: Neue Features in Java 15

(Melde dich für den HappyCoders-Newsletter an,
um sofort über neue Teile informiert zu werden.)

Bonus:

Java-Version unter Windows umschalten

Java 12, veröffentlicht am 19. März 2019, ist das erste "Zwischen-Release" nach dem letzten Long-Term-Support (LTS)-Release, Java 11.

Die Änderungen an Java 12 fallen im Vergleich zu den vorherigen Versionen etwas übersichtlicher aus. Zum ersten Mal seit Java 7 gibt es keine Änderung an der Sprache selbst.

Ich habe die Änderungen nach Relevanz für die tägliche Entwicklerarbeit sortiert. Am Beginn des Artikels stehen Erweiterungen der Klassenbibliothek. Es folgen Performance-Verbesserungen, experimentelle und Preview-Features und zuletzt kleinere Änderungen, mit denen du als Entwickler wahrscheinlich nicht konfrontiert wirst.

Neue String- und Files-Methoden

Nachdem wir in Java 11 einige neue String-Methoden und die Methoden Files.readString() und writeString() erhalten haben, haben die JDK-Entwickler beide Klassen für Java 12 nochmals erweitert.

String.indent()

Um einen String einzurücken, mussten wir uns bisher immer eine kleine Hilfsmethode schreiben, die die gewünschte Anzahl Leerzeichen vor den String setzte. Sollte das über mehrere Zeilen funktionieren, wurde die Methode entsprechend komplex.

Java 12 hat mit String.indent() eine solche Methode eingebaut. Das folgende Beispiel zeigt, wie ein mehrzeiliger String um 4 Zeichen eingerückt wird:

String s = "I amna multilinenString."; System.out.println(s); System.out.println(s.indent(4));
Code-Sprache: Kotlin (kotlin)

Das Programm gibt folgendes aus:

I am a multiline String. I am a multiline String.
Code-Sprache: Klartext (plaintext)

String.transform()

Die neue Methode String.transform() wendet eine beliebige Funktion auf einen String an und gibt den Rückgabewert der Funktion zurück. Hier ein paar Beispiele:

String uppercase = "abcde".transform(String::toUpperCase); Integer i = "12345".transform(Integer::valueOf); BigDecimal big = "1234567891011121314151617181920".transform(BigDecimal::new);
Code-Sprache: Java (java)

Wenn du dir den Quellcode von String.transform() anschaust, wirst du feststellen, dass hier keine Raketenwissenschaft am Werk ist, sondern die übergebene Methodenreferenz als Function interpretiert und der String an deren apply()-Methode übergeben wird:

public <R> R transform(Function<? super String, ? extends R> f) { return f.apply(this); }
Code-Sprache: Java (java)

Warum sollte man dann transform() verwenden statt einfach folgendes zu schreiben?

String uppercase = "abcde".toUpperCase(); Integer i = Integer.valueOf("12345"); BigDecimal big = new BigDecimal("1234567891011121314151617181920");
Code-Sprache: Java (java)

Der Vorteil von String.transform() liegt darin, dass die anzuwendende Funktion dynamisch zur Laufzeit bestimmt werden kann, während bei der letzten Schreibweise die Konvertierung zur Compile-Zeit festgelegt wird.

Files.mismatch()

Mit der Methode Files.mismatch() kann der Inhalt zweiter Dateien verglichen werden.

Die Methode gibt -1 zurück, wenn beide Dateien gleich sind. Ansonsten gibt sie die Position des ersten Bytes an, in dem sich beide Dateien unterscheiden. Wird das Ende einer Datei erreicht, bevor ein Unterschied festgestellt wurde, wird die Länge dieser Datei zurückgegeben.

(Die neuen String- und Files-Methoden sind nicht in einem JDK Enhancement Proposal definiert.)

Der Teeing Collector

Für manche Anforderungen möchte man einen Stream nicht mit einem, sondern mit zwei Collectoren terminieren und das Ergebnis beider Collectoren kombinieren.

In folgendem Beispiel-Quellcode möchten wir aus einem Stream von Zufallszahlen die Differenz von größter zu kleinster Zahl bestimmen (wir verwenden das in Java 10 eingeführte Optional.orElseThrow(), um eine Markierung als "Code Smell" zu vermeiden):

Stream<Integer> numbers = new Random().ints(100).boxed(); int min = numbers.collect(Collectors.minBy(Integer::compareTo)).orElseThrow(); int max = numbers.collect(Collectors.maxBy(Integer::compareTo)).orElseThrow(); long range = (long) max - min;
Code-Sprache: Java (java)

Das Programm compiliert zwar, bricht allerdings zur Laufzeit mit einer Exception ab:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) at eu.happycoders.sandbox.TeeingCollectorTest.main(TeeingCollectorTest.java:12)
Code-Sprache: Klartext (plaintext)

Der Exception-Text lässt uns unmissverständlich wissen, dass wir einen Stream nur einmal terminieren dürfen.

Wie können wir die Aufgabe lösen?

Eine Variante wäre es einen eigenen Collector zu schreiben, der Minimum und Maximum in einem int-Array mit zwei Elementen akkumuliert:

Stream<Integer> numbers = new Random().ints(100).boxed(); int[] result = numbers.collect( () -> new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE}, (minMax, i) -> { if (i < minMax[0]) minMax[0] = i; if (i > minMax[1]) minMax[1] = i; }, (minMax1, minMax2) -> { if (minMax2[0] < minMax1[0]) minMax1[0] = minMax2[0]; if (minMax2[1] > minMax1[1]) minMax1[1] = minMax2[1]; }); long range = (long) result[1] - result[0];
Code-Sprache: Java (java)

Das ist zum einen recht aufwändig, zum anderen nicht gut lesbar.

Mit dem in Java 12 eingeführten "Teeing Collector" geht es einfacher. Wir können zwei Collectoren angeben (sogenannte Downstream-Collectoren) und eine Merger-Funktion, die die Ergebnisse der beiden Collectoren kombiniert:

Stream<Integer> numbers = new Random().ints(100).boxed(); long range = numbers.collect( Collectors.teeing( Collectors.minBy(Integer::compareTo), Collectors.maxBy(Integer::compareTo), (min, max) -> (long) max.orElseThrow() - min.orElseThrow()));
Code-Sprache: Java (java)

Deutlich eleganter und lesbarer, oder?

Zum Abschluss die Frage: Warum heißt dieser Collector eigentlich "Teeing Collector"?

Der Name kommt von der englischen Aussprache des Buchstabens "T", da die grafische Darstellung des Collectors wie ein ... "T" aussieht:

Teeing Collector

(Auch zum Teeing Collector gibt es kein JDK Enhancement Proposal.)

Support for Compact Number Formatting

Mit der statischen Methode NumberFormat.getCompactNumberInstance() können wir einen Formatierer für die sogenannte "kompakte Zahlenformatierung" erzeugen. Das ist eine für den Menschen leicht lesbare Form, wie z. B. "2 Mio." oder "3 Milliarden".

Das folgende Beispiel zeigt, wie einige Zahlen einmal in der kurzen und einmal in der langen kompakten Form dargestellt werden:

NumberFormat nfShort = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); NumberFormat nfLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG); System.out.println(" 1.000 short -> " + nfShort.format(1_000)); System.out.println(" 456.789 short -> " + nfShort.format(456_789)); System.out.println(" 2.000.000 short -> " + nfShort.format(2_000_000)); System.out.println("3.456.789.000 short -> " + nfShort.format(3_456_789_000L)); System.out.println(); System.out.println(" 1.000 long -> " + nfLong.format(1_000)); System.out.println(" 456.789 long -> " + nfLong.format(456_789)); System.out.println(" 2.000.000 long -> " + nfLong.format(2_000_000)); System.out.println("3.456.789.000 long -> " + nfLong.format(3_456_789_000L));
Code-Sprache: Java (java)

Das Programm gibt folgendes aus:

1.000 short -> 1.000 456.789 short -> 456.789 2.000.000 short -> 2 Mio. 3.456.789.000 short -> 3 Mrd. 1.000 long -> 1 Tausend 456.789 long -> 457 Tausend 2.000.000 long -> 2 Millionen 3.456.789.000 long -> 3 Milliarden
Code-Sprache: Klartext (plaintext)

"Compact Number Formats" ist im gleichnamigen Unicode-Standard definiert.

(Ein JDK Enhancement Proposal existiert nicht für "Compact Number Formatting".)

Performance-Verbesserungen

Die folgenden Verbesserungen sorgen dafür, dass unsere Java-Applikationen schneller starten, niedrigere Garbage-Collector-Latenzen und einen besseren Memory-Footprint haben.

Default CDS Archives

Im Artikel über Java 10 findest du eine Beschreibung von Class-Data Sharing (CDS).

Um Class-Data Sharing zu aktivieren, musstest du bisher für jede Java-Installation einmalig das Kommando java -Xshare:dump ausführen, wodurch die Shared-Archive-Datei classes.jsa generiert wurde.

Mit dem JDK Enhancement Proposal 341 wird das JDK auf 64-Bit-Architekturen ab sofort mitsamt dieser Datei ausgeliefert, so dass die Ausführung von java -Xshare:dump nicht mehr nötig ist und Java-Anwendungen standardmäßig das Default-CDS-Archiv verwenden.

Abortable Mixed Collections for G1

Eines der Ziele des G1 Gargabe Collectors ist es, bei denjenigen Aufräumarbeiten, die nicht parallel zur Anwendung erfolgen können, vorgegebene maximale Pausezeiten einzuhalten – also die Anwendung nicht länger als die vorgegebene Zeit zu stoppen.

Beim G1 wird diese Zeit mit dem Parameter -XX:MaxGCPauseMillis festgelegt. Wird der Parameter nicht angegeben, liegt die maximale Pausezeit bei 200 ms.

G1 verwendet eine Heuristik, um eine Menge von Heap-Regionen zu bestimmen, die während einer solchen "Stop-the-World"-Phase aufgeräumt werden (das sogenannte "Collection Set").

Insbesondere bei "Mixed Collections" (d. h. beim Aufräumen von Regionen junger und alter Generationen) kann es – insbesondere, wenn sich das Verhalten der Anwendung verändert – vorkommen, dass die Heuristik ein zu großes Collection Set bestimmt und damit die Anwendung länger unterbrochen wird als vorgesehen.

JDK Enhancement Proposal 344 optimiert die Mixed Collections dahingehend, dass bei wiederholtem Überschreiten der maximalen Pausezeit das Collection Set in einen obligatorischen und einen optionalen Teil aufgeteilt wird. Der obligatorische Teil wird ununterbrechbar ausgeführt – und der optionale Teil in kleinen Schritten, bis die maximale Pausezeit erreicht ist.

Währenddessen versucht der Algorithmus die Heuristik so anzupassen, dass sie baldmöglichst wieder Collection Sets bestimmt, die in der vorgegebenen Pausezeit abarbeitbar sind.

Promptly Return Unused Committed Memory from G1

In Umgebungen, in denen man für den tatsächlich genutzten Arbeitsspeicher bezahlt, ist es wünschenswert, dass der Garbage Collector unbenutzten Speicherplatz schnell wieder an das Betriebssystem zurückgibt.

Der G1 Garbage Collector kann zwar Speicherplatz zurückgeben, allerdings nur während der Garbage-Collection-Phase. Wenn jedoch die Heap-Belegung oder die aktuelle Rate der Objekt-Allokationen keinen Garbage-Collection-Zyklus triggert, wird auch kein Arbeitsspeicher zurückgegeben.

Nehmen wir an, wir haben eine Applikation, die nur einmal am Tag einen speicherintensiven Batch-Prozess ausführt, den Rest der Zeit aber ziemlich inaktiv ist. So gibt es nach Abarbeitung des Batch-Prozesses keinen Grund für einen Garbage-Collection-Zyklus, und wir bezahlen für den größten Teil des Tages für Arbeitsspeicher, in dem unbenutzte Objekte liegen (rot markierter Bereich):

JEP 346: Speicherbelegung ohne periodische GCs
JEP 346: Speicherbelegung ohne periodische GCs

JEP 346 bietet eine Lösung für dieses Problem. Wenn die Anwendung inaktiv ist, wird in regelmäßigen Abständen ein paralleler Garbage-Collection-Zyklus gestartet, der ggf. nicht mehr benötigten Speicher wieder freigibt.

Dieses Feature ist standardmäßig deaktiviert. Du kannst es aktivieren, indem du über den Parameter -XX:G1PeriodicGCInterval ein Intervall in Millisekunden angibst, in dem G1 prüfen soll, ob ein solcher Zyklus gestartet werden soll. Der Speicher wird so zügig wieder zurückgegeben:

JEP 346: Speicherbelegung mit periodischen GCs
JEP 346: Speicherbelegung mit periodischen GCs

Experimentelle und Preview-Features

Dieser Abschnitt listet experimentelle und Preview-Features auf, also Features, die noch im Entwicklungsstadium sind und ggf. anhand von Feedback aus der Java-Community bis zum finalen Release noch verändert werden.

Anstatt detailliert auf diese Features einzugehen, werde ich auf die Java-Version verweisen, in der die jeweiligen Features als "production-ready" released werden.

Switch Expressions (Preview)

Dank JDK Enhancement Proposal 325 können switch-Anweisungen vereinfacht werden, indem mehrere Fälle mit Kommata getrennt werden und mit der Pfeil-Notation die fehleranfälligen break-Statements wegfallen können:

switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); }
Code-Sprache: Java (java)

Darüberhinaus können mit switch-Ausdrücken ("expressions") einer Variablen Fall-abhängige Werte zugewiesen werden:

int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; };
Code-Sprache: Java (java)

switch-Ausdrücke können auch mit der herkömmlichen Schreibweise (mit Doppelpunkt und break) notiert werden. Dabei wird der zurückzugebende Wert hinter dem break-Keyword angegeben:

int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY: break 6; case TUESDAY: break 7; case THURSDAY, SATURDAY: break 8; case WEDNESDAY: break 9; };
Code-Sprache: Java (java)

(Achtung: break wird im nächsten Preview durch yield ersetzt werden.)

Switch Expressions werden in Java 14 produktionsreif sein.

Um Switch Expressions bereits in Java 12 einzusetzen, musst du sie entweder in deiner IDE aktivieren (in IntelliJ geht das über File→Project Structure→Project Settings→Project→Project language level) oder mit dem Parameter --enable-preview beim Aufruf der javac- und java-Kommandos.

Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)

In Java 11 wurde Oracles "Z Garbage Collector" als experimentelles Feature vorgestellt.

Mit Java 12 kommt ein weiterer Low-Latency Garbage Collector hinzu: der von Red Hat entwickelte "Shenandoah". Genau wie ZGC hat auch Shenandoah das Ziel die Pausezeiten von Full GCs zu minimieren.

Du kannst Shenandoah über folgende Option in der java-Befehlszeile aktivieren:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

Shenandoah und ZGC werden in Java 15 Produktionsreife erreichen. Im entsprechenden Teil dieser Serie werde ich beide Garbage Collectoren detaillierter beschreiben.

(Dieses experimentelle Release ist im JDK Enhancement Proposal 189 definiert.)

Sonstige Änderungen in Java 12 (die man als Java-Entwickler nicht unbedingt kennen muss)

In diesem Abschnitt liste ich Änderungen auf, die die tägliche Arbeit der meisten Java-Entwicklerinnen und -Entwickler nicht betreffen werden. Es ist aber sicherlich nicht verkehrt, die Änderungen einmal überflogen zu haben.

Unicode 11

Nachdem in Java 11 Support für Unicode 10 integriert wurde, wurde der Support in Java 12 auf Unicode 11 angehoben. Das bedeutet, dass insbesondere die Klassen String und Character mit den neuen Zeichen, Codeblöcken und Schriftsystemen umgehen können müssen.

Ein Beispiel dazu findest du im zuvor verlinkten Abschnitt über Unicode 10.

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

Microbenchmark Suite

Bis dato wurden Microbenchmarks für die JDK-Klassenbibliothek als separates Projekt verwaltet. Diese Benchmarks messen regelmäßig die Leistung der JDK-Klassenbibliothek und werden z. B. verwendet um bei neuen Java-Releases sicherzustellen, dass JDK-Methoden nicht langsamer geworden sind.

Mit dem JDK Enhancement Proposal 230 wird die bestehende Sammlung an Microbenchmarks in den JDK-Quellcode verschoben, um die Ausführung und Weiter.entwicklung der Tests zu vereinfachen.

JVM Constants API

Im Konstantenpool ("constant pool") einer .class-Datei werden Konstanten gespeichert, die beim Compilieren einer .java-Datei anfallen. Das sind zum einen Konstanten, die im Java-Code definiert sind, wie z. B. der String "Hello world!", aber auch die Namen referenzierter Klassen und Methoden (z. B. "java/lang/System", "out" und "println"). Jede Konstante ist einer Nummer zugeordnet, auf die der Bytecode der .class-Datei referenziert.

Der JDK Enhancement Proposal 334 soll es erleichtern Java-Programme zu schreiben, die wiederum JVM-Bytecode lesen oder schreiben. Dazu werden neue Interfaces und Klassen zur Verfügung gestellt, mit der die Konstanten des Constant Pools abgebildet werden können.

Diese Interfaces und Klassen befinden sich im Paket java.lang.constant und bilden eine Hierarchie, die mit dem Interface ConstantDesc beginnt. Ein "Hello World!" bspw. wird durch die String-Klasse abgebildet, die seit Java 12 eben auch dieses Interface implementiert (genau wie Integer, Long, Float und Double).

Komplizierter wird es bei Konstanten, die Referenzen auf Klassen und deren Methoden darstellen. Die Reflection-Klassen Class und MethodHandle können wir nicht verwenden, da wir die referenzierten Klassen und Methoden gar nicht unbedingt kennen, sondern eben nur deren Namen, Parameter und Rückgabewerte.

Genau dafür gibt es nun (u. a.) die Klassen ClassDesc, um eine Klasse zu bezeichnen, und MethodHandleDesc und MethodTypeDesc, um eine Methode zu bezeichnen.

Weitere Details dieses doch eher exotischen Features würden den Rahmen dieses Artikels sprengen.

One AArch64 Port, Not Two

Im JDK existieren bis dato zwei unterschiedliche Portierungen für 64-Bit ARM-CPUs:

  • "arm64" – entwickelt von Oracle (als Erweiterung der 32-Bit-ARM-Portierung "arm")
  • "aarch64" – zeitgleich, aber unabhängig entwickelt von Red Hat

Mit dem JDK Enhancement Proposal 340 wird der Port von Oracle entfernt, um die Entwicklungsresourcen auf eine Portierung zu konzentrieren.

Vollständige Liste aller Änderungen in Java 12

Dieser Artikel hat alle Features von Java 12 vorgestellt, die in JDK Enhancement Proposals definiert sind, sowie Erweiterungen an der JDK-Klassenbibliothek, die keinem JEP zugeordnet sind.

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

Fazit

Die Änderungen in Java 12 sind recht überschaubar. Wir haben ein paar neue String- und Files-Methoden bekommen sowie den Teeing-Collector, mit dem wir einen Stream über zwei Collectoren terminieren und deren Ergebnisse kombinieren können.

Class-Data Sharing ist nun dank der auf 64-Bit-Systemen mitgelieferten classes.jsa-Shared-Archive-Datei standardmäßig aktiviert.

Der G1 Garbage Collector kann Mixed Collections abbrechen, wenn diese zu lange dauern. Nicht mehr benötigten Speicher gibt er zügig an das Betriebssystem zurück.

Mit Switch Expressions und dem Shenandoah Garbage Collector haven zwei experimentelle bzw. Preview-Features ebenfalls Einzug in Java 12 gehalten.

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

Möchtest du per E-Mail informiert werden, wenn der nächste Teil der Serie veröffentlicht wird? Dann klicke hier, um dich für den HappyCoders-Newsletter anzumelden.

Sven Woltmann
Über den Autor
Ich bin freiberuflicher Softwareentwickler mit über 20 Jahren Erfahrung in skalierbaren Java-Enterprise-Anwendungen. Mein Schwerpunkt liegt auf der Optimierung komplexer Algorithmen und auf fortgeschrittenen Themen wie Concurrency, dem Java Memory Model und Garbage Collection. Hier auf HappyCoders.eu möchte ich dir helfen, ein besserer Java-Programmierer zu werden. Lies mehr über mich hier.

Schreibe einen Kommentar

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

Die folgenden Artikel könnten dir auch gefallen