Java 21 - JAX https://jax.de/tag/java-21/ Java, Architecture & Software Innovation Wed, 02 Oct 2024 12:46:16 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Java 21 und die Helden von heute https://jax.de/blog/java21-und-die-helden-von-heute/ Wed, 15 Nov 2023 09:43:45 +0000 https://jax.de/?p=89073 In Java 21 sind viele neue Features enthalten. Doch neben den hippen, großartigen Features für Parallelität und funktionaler Programmierung gibt es auch einige eher unscheinbare, die einen großen Einfluss haben werden: Wir nennen sie Hidden Heroes.

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
In diesem Artikel werden „Generational ZGC“ (JEP 439), „Key Encapsulation Mechanism API“ (JEP 452) und „Code Snippets in Java Doc“ (JEP 413) vorgestellt. Beim ersten JEP handelt es sich um eine kleine Anpassung an den neuem Z Garbage Collector, der einen riesigen Performance Boost auf jeder Größe von Heap verspricht. Beim zweiten geht es um ein neues API mit dem Schlüssel für eine besonders sichere symmetrische Verschlüsselung, die mit Public/Private-Key-Methoden übertragen werden kann, und im letzten um die Einbindung von Codebeispielen in der Java-API-Dokumentation.

JEP 439: Generational ZGC

Eine Instanz auf dem Heap kann entfernt werden, wenn sie nicht mehr benötigt wird. In hardwarenahen Sprachen sind die Entwicklerinnen und Entwickler dafür zuständig, den korrekten, sicheren Zeitpunkt zu bestimmen. Der sichere Zeitpunkt ist der, wenn keine andere Instanz mehr eine Referenz auf die Instanz gespeichert hat. Durch Call-by-Reference-Aufrufe von Methoden können sich diese Referenzen sehr weit im System verbreiten und die Analyse wird komplex. Die manuelle Bestimmung des Zeitpunkts der sicheren Entfernung ist sehr fehleranfällig und deswegen setzt die JVM, wie viele moderne Ökosysteme, auf das Konzept des Garbage Collector, kurz GC. Ein Garbage Collector überwacht dabei automatisiert den Speicher und entfernt Instanzen nach dem sicheren Zeitpunkt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Hidden Heroes

Hidden Heroes leitet der Autor von dem Begriff „Hidden Champions“ ab, der von Hermann Simon geprägt wurde. Hidden Champions bezeichnen kleine Firmen, die Weltmarktführer sind und wenig Wahrnehmung in der Öffentlichkeit haben. Analog dazu sind Hidden Heroes Java-Features, die große Auswirkungen auf das JDK-Environment haben, aber gefühlt zu wenig Aufmerksamkeit in der Community bekommen.

In der Java Virtual Machine gibt es seit längerem mehrere verschieden implementierte Garbage Collectors mit diversen Schwerpunkten. Sie alle haben die GC-Pause gemein, in der die Anwendung pausiert werden muss:

  • Serial GC ist der einfachste GC. Er benutzt nur einen Thread und pausiert die Applikation, während er läuft. Er ist für Einzelkernsysteme und nicht für Hardware mit mehreren Kernen geeignet, da kein Nutzen aus mehreren Rechenkernen gezogen werden kann.
  • Parallel GC[1] verhält sich wie Serial GC, verwendet aber mehrere Kerne und ist damit eine bessere Alternative für Anwendungen auf Mehrkernhardware. Wie auch bei Serial GC ist die Länge der GC-Pause primär abhängig von der Größe des Heap-Speichers.
  • G1 GC heißt eigentlich Garbage First GC [2] und wendet Partitionen auf dem Heap an. Die Partitionen werden entsprechend dem freien Speicher aufsteigend priorisiert und analysiert. G1 GC analysiert und entfernt Instanzen, bis die konfigurierte fixe Länge der GC-Pause verstrichen ist. Es werden oft nicht alle Partitionen komplett bearbeitet, denn das Ziel von G1 ist eine möglichst feste Pausenzeit. Zudem eignet er sich gut für Maschinen mit vielen Prozessoren und großen Heaps.

Relativ neu ist der Z Garbage Collector [3], der in Java 15 eingeführt wurde, kurz ZGC genannt. Der ZGC führt die arbeitsintensive Analyse des Heap parallel zur Ausführung der Anwendung in eigenen Threads durch. Dadurch muss die Anwendung nur sehr kurz unterbrochen werden, um die Threads zu synchronisieren. Die GC-Pause ist dabei 1 ms lang und unabhängig von der analysierten Heapgröße. Realisiert wird die Parallelität mit Hilfe von Colored Pointer und Load Barriers. Colored Pointer sind eine Dekoration eines Pointers, einer Speicheradresse mit Metainformationen über den Status der Instanz. Load Barriers werden für Zugriffe auf Referenzen eingebaut. Sie werten Colored Pointer aus und führen potenziell notwendige Weiterleitungen durch, bevor die Anwendung auf die Instanz zugreift, um Adressänderungen zu verschleiern. Der hochoptimierte Algorithmus von ZGC wird im Artikel „Deep-dive of ZGC’s Architecture“ [4] auf Dev.java ausführlich beschrieben und ist in der Lage, sehr kleine und sehr große Heaps bis 16 TB effizient zu bearbeiten. Aktiviert werden kann ZGC durch den Kommandozeilenparameter -XX:+UseZGC. Bei der ersten Verwendung von ZGC wird empfohlen, auch GC-Logging (-Xlog:gc) zu aktivieren, um das Finetuning der Konfiguration zu ermöglichen. Neben der Anzahl der von ZGC verwendeten Threads (-XX:ConcGCThreads) und einigen Linux-spezifischen Parametern kann auch das Zurückgeben von Speicher an das Betriebssystem aktiviert werden (-XX:+ZUncommit).

Mit JEP 439: Generational ZGC [5] wird ZGC um die Partitionierung des Heap in Generationen, also je einen Bereich für „jung“ und „alt“, erweitert. Der Bereich für die jungen Instanzen unterteilt sich noch in „Eden“ und „Survivor“. Neu erzeugte Instanzen werden in der Regel in Eden erzeugt und, wenn sie den ersten Durchlauf des GC „überleben“, nach Survivor kopiert. Nachdem sie eine feste Anzahl GC-Läufe in Survivor „überlebt“ haben, werden Instanzen in den Bereich für alte Instanzen kopiert. Die Partitionierung ermöglicht die Anwendung der Weak Generational Hypothesis [6], die im Kern aussagt: „Junge Instanzen haben die Tendenz, jung zu sterben“. Als Konsequenz kann unter Instanzen im Bereich „jung“ und besonders unter denen im Bereich Eden vermutet werden, dass sie nicht mehr referenziert werden und zu entfernen sind. Im Bereich „alt“ sind wahrscheinlich kaum zu entfernende Instanzen enthalten und diese werden nicht so häufig analysiert. Durch die getrennte Behandlung verringert sich der durchschnittliche Aufwand der Analyse und der GC-Prozess wird effizienter. Aktiviert werden kann diese Partitionierung mit dem Parameter -XX:+ZGenerational. Zukünftig soll ZGC nur noch den Ansatz mit Generationen verwenden, dann ist der Parameter nicht mehr notwendig.

Mit seiner effizienten Behandlung von riesigen Heaps, der geringen GC-Pause und dem Fokus auf das Entfernen von jungen Instanzen ist Genrational ZGC optimiert für datenintensive Anwendungen, die eine kurze Antwortzeit erfordern. Damit kann Generational ZGC eine gute Wahl für moderne datengetriebene Systeme im Enterprise-Umfeld sein. Durch die hohe Komplexität und viel notwendiger Theorie in diesem Themenfeld fehlt es Generational ZGC ein wenig an Aufmerksamkeit, deswegen ist Generational ZGC ein Hidden Hero des Java-Ökosystems.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

JEP 452: Key Encapsulation Mechanism API

In der elektronischen Kommunikation wird an vielen Stellen auf Verschlüsselung gesetzt. Viele der heute gängigen Verfahren gelten als nicht sicher für ein Zeitalter mit Quantencomputern. An einem solchen Quantencomputer könnte zum Beispiel Mallroy forschen – und vor kurzen hatte er einen großen Durchbruch. Alice und Bob wollen deswegen eine Überraschungsparty für ihren Freund Mallroy organisieren. Damit Mallroy unwissend bleibt, wollen Alice und Bob nur über sichere, verschlüsselte Kanäle miteinander kommunizieren. Sie wollen zahlreiche Nachrichten austauschen und Mallroy könnte schon den Quantencomputer zur Verfügung haben, deswegen kommt nur ein effizientes und hochsicheres Verfahren in Frage.

Bei ihrer Recherche erfahren sie, dass es symmetrische und asymmetrische Verfahren gibt. Bei einem asymmetrischen Verschlüsselungsverfahren werden zwei verschiedene Schlüssel zum Ver- und Entschlüsseln verwendet. Der Schlüssel zum Verschlüsseln kann gefahrenlos übermittelt werden, leider sind diese Algorithmen in der Regel nicht hochperformant und auch nicht besonders sicher. Bei symmetrischen Verfahren wird derselbe Schlüssel zum Ver- und Entschlüsseln verwendet, sie punkten bei Effizienz und Sicherheit ganz klar. Leider benötigen beide Seiten das Wissen über den verwendeten Schlüssel und dieser kann, da geheim, nicht trivial versendet werden.

Im 2001 von Cramer und Shoup veröffentlichten Artikel „Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack“ [7] wird in § 7.1 der Mechanismus „Key Encapsulation“ beschrieben, mit dem es möglich ist, Schlüssel für symmetrische Verschlüsselungsverfahren unter Zuhilfenahme asymmetrischer Verschlüsselung sicher zu übertragen. Dieses unter der Abkürzung KEM bekannte Verfahren wird unter anderem vom BSI und der NIST in ihren Post-Quanten-Kryptografie-Konzepten als Basisbaustein betrachtet [8], [9]. In Abbildung 1 ist der KEM-Ablauf zwischen Alice und Bob dargestellt. Zuerst generiert Alice ein Schlüsselpaar aus öffentlichem und privatem Schlüssel. Der öffentliche Schlüssel wird an Bob übertragen. An dieser Stelle ist eine Transportverschlüsselung zwischen Alice und Bob wichtig, aber nicht Gegenstand des Verfahrens. Bob erzeugt einen zufälligen Schlüssel für das später verwendete symmetrische Verschlüsselungsverfahren und verschlüsselt diesen mit dem öffentlichen Schlüssel von Alice. Dieses als „Encupsulated“ bekannte Datenpaket wird an Alice zurückübertragen, und nur sie kann es mit ihrem privaten Schlüssel entschlüsseln. Von nun an können Alice und Bob ihre Nachrichten mit einer symmetrischen Verschlüsselung austauschen, das Schlüsselpaar wird nicht mehr gebraucht.

Abb. 1: Bob und Alice tauschen den Schlüssel für eine symmetrische Verschlüsselung (gelb) mit KEM aus

Da die Implementierung sicherheitsrelevanter Features und insbesondere von Verschlüsselungen ein hochkomplexes Feld ist, gleichzeitig aber die Verschlüsselung eine grundlegende Anforderung moderner Systeme ist, wird mit JEP 452 [10] ein API zur Durchführung eines KEM-Prozesses in Java 21 eingeführt. Das API folgt dem von der ISO 18033-2 definierten und oben beschriebenen Prozess. Dabei bilden drei Bausteine das Fundament für eine sichere Verschlüsselung. Der javax.crypto.KEM.Encapsulator verwendet den öffentlichen Schlüssel und den erzeugten symmetrischen Schlüssel, um ein Encapsulated zu erzeugen. Das Encapsulated wird durch eine Instance von javax.crypto.KEM.Encapsulated repräsentiert und kann direkt als Bytearray von Bob übertragen werden. Auf Alices Seite wird die Klasse javax.crypto.KEM.Decapsulator verwendet, um mit dem passenden privaten Schlüssel den symmetrischen Schlüssel zu erlangen.

Im nun folgenden Beispiel bilden die Methoden sendToBob/sendToAlice und retrieveFromBob/ retrieveFromAlice die jeweilig sendenden beziehungsweise empfangenden Enden der unsicheren Kommunikationskanäle zwischen Alice und Bob ab. Listing 1 zeigt, wie das Set-up des KEM-Prozesses bei Alice mit dem KeyPairGenerator implementiert wird. Die Methode KeyPairGenerator#getInstance(String) erzeugt einen Schlüsselpaarerzeuger für das RSA-Verfahren. Über ein Service Provider Interface ist es möglich, hier weitere Algorithmen zu hinterlegen. Mit dem Aufruf von KeyPairGenerator#generateKeyPair() wird ein Paar aus öffentlichem und privatem Schlüssel erzeugt. Dieses Paar wird bei Alice hinterlegt und der öffentliche Schlüssel an Bob versandt.

var keyGen = KeyPairGenerator.getInstance("RSA");
var pair = keyGen.generateKeyPair();

sendToBob(pair.getPublic());

In Listing 2 wird die erste Phase der Key Exchanges auf der Seite von Bob gezeigt. Nachdem der öffentliche Schlüssel von Alice empfangen wurde, wird mir KEM#getInstance(String) eine für RSA konfigurierte Implementierung gewählt. Neue Implementierungen können auch hier durch ein SPI bereitgestellt werden. Auf der konkreten KEM-RSA-Instanz wird mit der Methode KEM#newEncapsulator(java.security.PublicKey) eine Encapsulator-Instanz für RSA-KEM erzeugt. Der zufällig erzeugte Schlüssel kann und sollte über Encapsulated#key() abgerufen und bei Bob hinterlegt werden (hier in der Variable secret). Mit diesem Schlüssel wird die spätere symmetrische Verschlüsselung durchgeführt. Auf das zu versendende Encapsulate kann über die Methode Encapsulated#encapsulate() zugegriffen werden und es wird von Bob an Alice versendet.

var publicKey = retrieveFromAlice();

var encapsulator = KEM.getInstance("RSA-KEM").newEncapsulator(publicKey);
var secret = encapsulate.key();
var encapsulate = encapsulator.encapsulate();

sendToAlice(encapsulate.encapsulation());

In Listing 3 wird die zweite Phase der Key Encapsulation bei Alice gezeigt. Nachdem das Encapsulate von Bob empfangen und der private Schlüssel aus dem Schlüsselpaar des Set-ups entnommen wurde, kann mit dem Entpacken des symmetrischen Schlüssels begonnen werden. Zuerst wird wieder mit der Methode KEM#getInstance(String) die konkrete Implementierung von KEM für RSA-KEM geladen. Mit dem Aufruf KEM# newDecapsulator(PrivateKey) wird unter Zuhilfenahme des privaten Schlüssels der symmetrische Schlüssel entpackt.

byte[] encapsulate = retrieveFromBob();
var privateKey = pair.getPrivate();

var decapsulator = KEM.getInstance("RSA-KEM").newDecapsulator(privateKey);
var secret = decapsulator.decapsulate(encapsulate);

Von diesem Moment an ist das verwendete öffentliche und private Schlüsselpaar nicht mehr notwendig und kann gelöscht werden. Die Kommunikation kann nun sicher und effizient mit einem symmetrischen Verschlüsselungsverfahren durchgeführt werden. Mit diesem sehr neuen Feature in Java 21 rüstet die OpenJDK-Community das Ökosystem Java für das Post-Quantum-Zeitalter auf. Die Verwendung von Java in einer Welt voller Quantencomputer ist nun möglich und aus diesem Grund sollte dieses Feature mehr Aufmerksamkeit bekommen. Es ist auf jeden Fall ein Hidden Hero von Java 21.

JEP 413: Code Snippets in Java API Documentation

An Schnittstellen stellen Entwickler und Entwicklerinnen ganz eigene Anforderungen. Oft sind Benutzbarkeit und verständliche Dokumentation die wichtigsten Aspekte. Aus diesem Grund ist Javadoc als Dokumentationswerkzeug für Source Code bereits seit Java 1 Teil des JDK. Mit Javadoc können Kommentare mit beschreibenden Tags angereichert werden und in eine navigierbare und durchsuchbare Schnittstellendokumentation überführt werden. Neben Beschreibungen der Parameter und des Verhaltens ist es sinnvoll, die beabsichtigte Verwendung zu dokumentieren, um die Hürden der Einarbeitung zu verringern. Es haben sich einige unterschiedliche Ansätze etabliert:

  1. Separate Tutorials wie die Referenzdokumentation von Spring [11] oder Extensions Guides von Quarkus [12]. Hier liegt der Fokus eher auf der Verwendung des Frameworks und weniger auf dem API.
  2. Bei Code Snippets als HTML mit <pre>{@code } </pre> liegt das API im Fokus. Das Snippet wird leider nicht ansehnlich formatiert und muss bei jeder Änderung an der Schnittstelle bedacht werden. Ein Beispiel ist java.util.stream.Stream [13], [14].
  3. Unit Tests als Dokumentation zu betrachten, ist ein guter Ansatz, vorausgesetzt die Tests sind gut und die verwendenden Entwickler und Entwicklerinnen haben Zugriff auf den Code. Bedauerlicherweise fehlt bei diesem Ansatz oft die Verknüpfung zwischen Code und Tests.

Mit JEP 413: Code Snippets in der Java-API-Dokumentation [15] hält Java seit Version 18 eine Möglichkeit bereit, Source-Code-Auszüge mit Syntax-Highlighting und Testbarkeit zu vereinen, um Entwicklerinnen und Entwicklern das Einbinden von guten Beispielen in der API-Dokumentation zu ermöglichen. Um ein Snippet in einem Javadoc-Kommentar einzubinden, wird der neue {@snippet: …  }-Tag verwendet.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet :
 *   var kunde = new Privatkunde("Merlin", "");
 *   var wert = 1055d;
 *   // ...
 *   var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert);
 * }
 */

In Listing 4 wird in einem Inline Snippet die erwartete Interaktion mit dem API des MwStRechner aus dem Data-Oriented-Programming-Beispielprojekt des Autors [16] dargestellt. In der generierten Dokumentation ist der Bereich ab der neuen Zeile hinter dem Doppelpunkt bis zur letzten Zeile vor der schließenden Klammer als formatierter Source Code mit Syntax-Highlighting enthalten. Bei Inline Snippets gibt es zwei Randbedingungen:

  • Ein mehrzeiliger Kommentar mit /* */ ist nicht erlaubt.
  • Für jede geöffnete Klammer muss auch eine schließende enthalten sein.

Ohne diese Randbedingungen ist es dem Generator nicht möglich, die Passage korrekt zu konvertieren. Zusätzlich muss die syntaktische Korrektheit manuell geprüft und Schnittstellenänderungen beachtet werden. Für ein externes Snippet gelten diese Rahmenbedingungen allesamt nicht. Bei einem externen Snippet wird der Inhalt nicht im selben Kommentar angegeben, sondern aus einer vorhandenen Java-Datei entnommen.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet file="SwitchExpressionsSnippets.java" region="example"}
 */

// Datei: snippet-files/Snippets.java
class Snippets {
  public void snippet01() {
    void snippet01() {
    // @start region="example"
    // @replace region="replace" regex='(= new.*;)|(= [0-9]*d;)' replacement="= ..."
    // @highlight region="highlight" regex="\bMwStRechner\b"
    var kunde = new Privatkunde("Merlin", "[email protected]"); // @replace regex="var" replacement="Kunde"
    var wert = 1055d; // @replace regex="var" replacement="double"
    /* .. */
    var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert); // @link substring="PlainOOP.calculateMwSt" target="MwStRechner.PlainOOP#calculateMwSt"
    // @end @end @end
  }
}

In Listing 5 wird ein externes Snippet verwendet, das auf eine Datei Snippets.java (auch Listing 5) im Ordner snippet-files verweist. Dieser Ordner liegt direkt neben der Datei, in der das Snippet eingebunden wird und kann mit Hilfe der Konfiguration -snippet-path überschrieben werden. Den Pfad auf den Ordner mit dem Test zu konfigurieren, erscheint dem Autor als guter Standard. Dadurch ist es möglich, die geschriebenen Tests als Beispiele wiederzuverwenden. In Listing 2 ist zudem eine Region definiert. Dadurch werden nur bestimmte Bereiche der referenzierten Java-Datei verwendet und die Datei kann so Beispiele für verschiedene Anwendungsfälle enthalten.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Neben Bereichen sind in dem Snippet in Listing 5 noch weitere Anpassungen durchgeführt worden. Mit den @replace Tags werden per regulärem Ausdruck zuerst alle Initialisierungen durch ersetzt, da sie nicht direkt zum Beispiel beitragen. Das Schlüsselwort var wird in den entsprechenden Zeilen durch den Datentyp ersetzt. Hierbei wird keine Region angeben, also die Ersetzung nur auf diese Zeile angewandt. Mit dem Tag @highlight wird jedes Vorkommen von MwStRechner hervorgehoben und mit @link ein Link zur Methode MwStRechner.PlainOOP#calculateMwSt erstellt. In Abbildung 2 wird das Ergebnis der Javadoc-Generierung des Listings 5 gezeigt.

Abb. 2: Aus Listing 5 generierte Dokumentation mit Ersetzungen, Links und Highlights

Durch die Möglichkeit zur Verknüpfung zwischen Source Code und API-Dokumentation kann gewährleistet werden, dass auch Listings in Javadoc aktuell und von hoher Qualität sind. Die Dokumentation von APIs gewinnt dadurch an Qualität, Aktualität und einem hohen Grad an Verständlichkeit. Werden alle Tags in einem Snippet verwendet, wird das Lesen des zugrundeliegenden Codes erschwert. Das betrifft vor allem die Entwicklerinnen und Entwickler von Frameworks und Tools. Von ihrem Mehraufwand werden aber alle profitieren, deswegen sind Code Snippets in Java API Documentation auf jeden Fall ein Hidden Hero im Java-21-Ökosystem.

Zusammenfassung

In diesem Artikel wurden drei der vielen Hidden Heroes im JDK-Ökosystem abseits von Virtual Threads und Pattern Matching gezeigt. Es gibt noch einige mehr, wie beispielsweise Class Data Sharing und den Simple Web Server, aber das ist Material für einen weiteren Beitrag auf Konferenzen, User Groups oder für das Selbststudium.


Links & Literatur

[1] https://docs.oracle.com/en/java/javase/20/gctuning/parallel-collector1.html

[2] https://docs.oracle.com/en/java/javase/20/gctuning/garbage-first-garbage-collector-tuning.html

[3] https://docs.oracle.com/en/java/javase/20/gctuning/z-garbage-collector.html

[4] https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/

[5] https://openjdk.org/jeps/439

[6] https://docs.oracle.com/en/java/javase/17/gctuning/garbage-collector-implementation.html

[7] Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack, Crammer und Shoup 2001, https://eprint.iacr.org/2001/108.pdf

[8] https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Quantentechnologien-und-Post-Quanten-Kryptografie/Post-Quanten-Kryptografie/post-quanten-kryptografie_node.html

[9] https://csrc.nist.gov/News/2022/pqc-candidates-to-be-standardized-and-round-4

[10] https://openjdk.org/jeps/452

[11] https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

[12] https://quarkus.io/guides/resteasy-reactive

[13] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html

[14] https://github.com/openjdk/jdk/blob/4de3a6be9e60b9676f2199cd18eadb54a9d6e3fe/src/java.base/share/classes/java/util/stream/Stream.java#L52-L57

[15] https://openjdk.org/jeps/413

[16] https://github.com/MBoegers/DataOrientedJava

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
Neues in Java 21: JEP 430 und 431 https://jax.de/blog/neues-in-java-21-jep-430-und-431/ Wed, 04 Oct 2023 11:55:16 +0000 https://jax.de/?p=89017 Java 21 bringt viele Verbesserungen in Syntax und APIs. In diesem Beitrag stehen die beiden Neuerungen JEP 430: „String Templates (Preview)“ sowie JEP 431: „Sequenced Collections“ im Fokus. Durch diese Funktionalitäten wird Java komfortabler und angenehmer in der Handhabung und besser für die Zukunft gerüstet. String Templates adressieren das Aufbereiten textueller Informationen, die aus fixen und variablen Bestandteilen bestehen, und Sequenced Collections vereinfachen den Zugriff und die Verwaltung von Daten am Anfang und Ende einer Collection.

The post Neues in Java 21: JEP 430 und 431 appeared first on JAX.

]]>
Regelmäßig müssen Strings aus fixen und variablen Textbausteinen zusammengesetzt werden. Um das zu tun, gibt es verschiedene Varianten, angefangen bei der einfachen Konkatenation mit + bis hin zur formatierten Aufbereitung. Die mit Java 21 neu eingeführten String Templates (JEP 430) ergänzen die bisherigen Varianten um eine elegante Möglichkeit, Ausdrücke angeben zu können, die zur Laufzeit ausgewertet und passend in den String integriert werden. Allerdings sind die String Templates noch ein Preview-Feature und müssen beim Kompilieren und Ausführen explizit aktiviert werden.

Bisherige Vorgehensweise

Schauen wir kurz zurück. Um formatierte Ausgaben in Java zu erzeugen, verwenden viele Entwickler die folgenden Varianten, um aus Variablen und Textbausteinen zu kombinieren:

String result = "Calculation: " + x + " plus " + y + " equals " + (x + y);
System.out.println(result);

und

String resultSB = new StringBuilder().append("Calculation: ").append(x).append(" plus ").append(y).append(" equals ").append(x + y).toString();
System.out.println(resultSB);

Die Stringkonkatenation mit + ist leicht verständlich und oft auch durchaus lesbar. Die Lesbarkeit nimmt jedoch bei einer zunehmenden Anzahl von Verknüpfungen ab. Noch dramatischer ist der Effekt beim Einsatz eines StringBuffers oder StringBuilders und dessen Methode append(): Je mehr Elemente zu verknüpfen sind, desto unleserlicher und schwieriger nachvollziehbar wird das Konstrukt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Weitere, in der Praxis seltener anzutreffende Möglichkeiten sind die Methoden format() und formatted() aus der Klasse String. Beide nutzen die gleichen Platzhalter, variieren aber ein wenig in der Handhabung, wobei meine Präferenz bei formatted() liegt.

var resultF1 = String.format("Calculation: %d plus %d equals %d", x, y, x + y);
System.out.println(resultF1);

var resultF2 = "Calculation: %d plus %d equals %d".formatted(x, y, x + y);
System.out.println(resultF2);

Darüber hinaus existiert noch die Klasse java.text.MessageFormat. Von ihr werden zunächst per Konstruktoraufruf mit fixem Text sowie integrierten Platzhaltern Instanzen erzeugt und danach mit format() befüllt – sowohl die Platzhalter als auch die Angabe der Werte unterscheiden sich von den beiden vorherigen Varianten:

var msgFormat = new MessageFormat("Calculation: {0} plus {1} equals {2}");
System.out.println(msgFormat.format(new Object[] { x, y, x + y }));

Möglichkeiten anderer Programmiersprachen

Viele Programmiersprachen unterstützten alternativ zur Stringkonkatenation die sogenannte Stringinterpolation oder auch formatierte Strings. Dabei kommt ein String zum Einsatz, in den an verschiedenen Positionen speziell gekennzeichnete Platzhalter integriert sind, die dann zur Laufzeit durch die entsprechenden Werte ersetzt werden. Das gilt beispielsweise für die f-Strings in Python. Etwas Ähnliches bieten Kotlin, Swift und C# durch unterschiedliche Notationen mit Platzhaltern:

  • Python: “Calculation: {x} + {y} = {x + y}”
  • Kotlin: “Calculation: $x + $y = ${x + y}”
  • Swift: “Calculation: \(x) + \(y) = \(x + y)”
  • C#: $”Calculation: {x} + {y}= {x + y}”

In allen Fällen werden die Platzhalter im String durch die entsprechenden Werte der Variablen, insbesondere auch von Berechnungen, ersetzt.

Die Alternative: String Templates (Preview)

Mit JEP 430 werden String Templates eingeführt, die wir uns nun anschauen wollen. Bilden wir das einführende Beispiel damit nach – aber bitte bedenken Sie, dass es sich um ein Preview-Feature handelt, das extra aktiviert werden muss, etwa wie folgt für die JShell:

$ jshell --enable-preview
|  Willkommen bei JShell - Version 21
|  Geben Sie für eine Einführung Folgendes ein: /help intro

Lernen wir zunächst die grundlegende Syntax kennen. Dabei muss dem String Template das Kürzel STR vorangestellt werden. Es aktiviert einen sogenannten Template Processor, der dann die Platzhalter im Format \{expression} ersetzt, im einfachsten Fall einfache Variablennamen durch deren Wert (Listing 1).

jshell> int x = 47
x ==> 47

jshell> int y = 11
y ==> 11

jshell> System.out.println(STR."Calculation: \{x} + \{y} = \{x + y}")
Calculation: 47 + 11 = 58

Bei der Angabe der Ausdrücke ist man nicht auf einfache mathematische Operationen beschränkt, sondern es lassen sich beliebige Java-Aufrufe einfügen, also auch Methoden (Listing 2).

jshell> int x = 7
x ==> 7

jshell> int y = 2
y ==> 2

jshell> STR."Berechnung: \{x} mal \{y} = \{Math.multiplyExact(x, y)}"
$3 ==> "Berechnung: 7 mal 2 = 14"

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Praxistipp

Bei der Einführung gab es diverse Diskussionen über das Format der Ausdrücke. Weil viele Third-Party-Bibliotheken etwa $, # oder {} dafür nutzen, hat man sich für ein Format entschieden, das nur innerhalb von String Templates gültig ist. Interessanterweise ist das STR per Definition public static final und automatisch in jeder Java-Datei bereits vorhanden.

Praktischerweise kann man String Templates auch in Kombination mit Textblöcken nutzen (Listing 3).

jshell> int statusCode = 201
statusCode ==> 201

jshell> var msg = "CREATED"
msg ==> "CREATED"

jshell> String json = STR."""
  ...>     {
  ...>       "statusCode": \{statusCode},
  ...>       "msg": "\{msg}"
  ...>     }""";
  ...> 

json ==> "{\n  \"statusCode\": 201,\n  \"msg\": \"CREATED\"\n}"

Tatsächlich gefällt mir in dem Zusammenhang die Methode formatted() ziemlich gut, und ich würde sie String Templates sogar vorziehen:

jshell> String json = STR."""
  ...> {
  ...>     "statusCode": %d,
  ...>     "msg": "%s"
  ...> }""".formatted(statusCode, msg);
json ==> "{\n    \"statusCode\": 201,\n    \"msg\": \"CREATED\"\n}"

String Templates lassen sich auch zur Aufbereitung einer simplen HTML-Seite nutzen (Listing 4). Nach dem Ersetzen entsteht das HTML in Listing 5.

String title = "My First Web Page";
String text = "My Hobbies:";
var hobbies = List.of("Cycling", "Hiking", "Shopping");
String html = STR."""
<html>
  <head><title>\{title}</title></head>
  <body>
    <p>\{text}</p>
    <ul>
      <li>\{hobbies.get(0)}</li>
      <li>\{hobbies.get(1)}</li>
      <li>\{hobbies.get(2)}</li>
    </ul>
  </body>
</html>""";
<html>
  <head><title>My First Web Page</title></head>
  <body>
    <p>My Hobbies:</p>
    <ul>
      <li>Cycling</li>
      <li>Hiking</li>
      <li>Shopping</li>
    </ul>
  </body>
</html>

Speichert man das Ganze als Datei und öffnet diese in einem Browser, so erhält man in etwa die Darstellung in Abbildung 1.

Abb. 1: Simple HTML-Seite mit String Templates

Besonderheiten

Interessanterweise ist es möglich, auch komplexere Aktionen in einem Platzhalter auszuführen. Als eher einfaches Beispiel haben wir bereits einen Methodenaufruf gesehen. Es sind aber auch Aktionen wie die in Listing 6 möglich, also ein Postinkrement, der ?-Operator oder Zugriffe auf das Date and Time API – in diesem Muster lassen sich dann einfache Anführungszeichen ohne Escaping nutzen.

int index = 0;
var modified = STR."\{index++}, \{index++}, \{index++}, \{index++}";
System.out.println(modified);

String filePath = "tmp.dat";
File file = new File(filePath);
  String old = "The file " + filePath + " " + file.exists() ? "does" : "does not" + " exist");
 String msg = STR. "."The file \{filePath} \{file.exists() ? 
                  "does" : "does not"} exist";

String currentTime = STR."Current time: \{DateTimeFormatter.ofPattern("HH:mm").format(LocalTime.now())}";
System.out.println(currentTime);

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Neben STR existieren weitere vordefinierte Prozessoren, etwa FMT, um eine Ausgabe wie mit String.format() zu erzielen. Betrachten wir dazu die Methode in Listing 7.

private static void alternativeStringProcessors() {
  int x = 47;
  int y = 11;
  String calculation1 = FMT."%6d\{x} + %6d\{y} = %6d\{x + y}";
  System.out.println("fmt calculation 1: " +calculation1);

  float base = 3.0f;
  float addon = 0.1415f;

  String calculation2 = FMT."%2.4f\{base} + %2.4f\{addon}" +
                        FMT."= %2.4f\{base + addon}";
  System.out.println("fmt calculation 2 " + calculation2);

    String calculation3 = FMT."Math.PI * 1.000 = %4.6f\{Math.PI * 1000}";
    System.out.println("fmt calculation 3 " + calculation3);
}

Als Ausgabe erhält man:

fmt calculation 1:     47  +     11 =     58
fmt calculation 2: 3.0000  + 0.1415 = 3.1415
fmt calculation 3: Math.PI * 1.000 = 3141.592654

Während STR standardmäßig verfügbar ist, muss man FMT auf geeignete Weise importieren:

import static java.util.FormatProcessor.FMT;

Eigene Stringprozessoren

Die Arbeitsweise von STR und FMT ist eingängig. Möchte man selbst steuernd eingreifen, ist es sogar möglich, eigene Template Processors zu erstellen, indem man das Interface StringTemplate.Processor implementiert. Dabei existieren diverse Methoden, die man für eine angepasste Variante nutzen kann. Folgendes Beispiel zeigt sowohl STR als auch die eigene Ausprägung in Form der Klasse MyProcessor im Einsatz:

String name = "Michael";
int age = 52;
System.out.println(STR."Hello \{name}. You are \{age} years old.");

var myProc = new MyProcessor();
System.out.println(myProc."Hello \{name}. You are \{age} years old.");

Betrachten wir die Ausgaben, die auch verschiedene Resultate der internen Methoden zeigen, um das Verständnis dafür zu erhöhen, wie die Abläufe und Verarbeitungsschritte innerhalb eines Template Processor erfolgen. Ziel des eigenen Prozessors ist es, die Werte jeweils speziell mit >> und << zu markieren:

Hello Michael. You are 52 years old.
-- process() --
=> fragments: [Hello , . You are ,  years old.]
=> values: [Michael, 52]
=> interpolate: Hello Michael. You are 52 years old.
Hello >>Michael<<. You are >>52<< years old.

Tatsächlich ist gar nicht viel Arbeit in der Methode process() zu erledigen, insbesondere würde man die Ausgaben weglassen, hier dienen sie lediglich dem Verständnis und der leichteren Nachvollziehbarkeit (Listing 8).

static class MyProcessor implements StringTemplate.Processor<String,
                                    IllegalArgumentException> {
    @Override
    public String process(StringTemplate stringTemplate) 
                  throws IllegalArgumentException {
        System.out.println("\n-- process() --");
        System.out.println("=> fragments: " + stringTemplate.fragments());
        System.out.println("=> values: " + stringTemplate.values());
        System.out.println("=> interpolate: " + stringTemplate.interpolate());
        System.out.println();

        var adjustedValues = stringTemplate.values().stream().
                                            map(str -> ">>" + str + "<<").
                                            toList();

        return StringTemplate.interpolate(stringTemplate.fragments(),
                                          adjustedValues);
    }
}

JEP 431: Sequenced Collections

Das Java Collections API ist eins der ältesten und am besten konzipierten APIs im JDK und bietet die drei Haupttypen List, Set und Map. Was allerdings fehlt, ist so etwas wie eine geordnete Reihenfolge von Elementen in Form eines Typs. Einige Collections haben eine sogenannte Encounter Order/Iteration Order (Begegnungsreihenfolge), d. h., es ist definiert, in welcher Reihenfolge die Elemente durchlaufen werden, etwa

  • List: indexbasiert, von vorne nach hinten
  • TreeSet: indirekt durch Comparable oder übergebenen Comparator definiert
  • LinkedHashSet: gemäß der Einfügereihenfolge

Dagegen definiert etwa HashSet keine derartige Encounter Order – und auch HashMap tut das nicht.

 

Auf Basis der Encounter Order ist auch das erste bzw. letzte Element definiert. Zudem lässt sich so eine Append-Funktionalität für vorn und hinten realisieren. Darüber hinaus ergibt sich auch die umgekehrte Reihenfolge. All das wird von Sequenced Collections adressiert.

Einführende Beispiele

Bis Java 21 war es mühsam, auf das letzte Element einer Collection zuzugreifen. Schlimmer noch – es existieren diverse, jeweils vom Typ der Collection abhängige unterschiedliche Wege:

var lastListElement = list.get(list.size() - 1);
var lastSortedElement = sortedSet.last();
var lastDequeElement = deque.getLast();

Fast ebenso unhandlich waren mitunter Zugriffe auf das erste Element, insbesondere für Sets:

var firstElement = list.get(0);
var firstUnordered = hashSet.iterator().next();
var firstOrdered = treeSet.iterator().next();
var firstLhs = linkedHashSet.iterator().next();

Sich all diese Besonderheiten zu merken, ist mühsam und fehleranfällig. Selbst mit IDE-Unterstützung bleibt noch die mangelnde Eleganz. Schauen wir uns nun Sequenced Collections an und wie diese nicht nur diese Aufgabenstellung, sondern auch das Anfügen und Löschen von Elementen vereinfachen.

Abhilfe: Sequenced Collections

Bislang bietet Java keinen Typ, der eine geordnete Folge von Elementen repräsentiert. Wie angedeutet, füllt Java 21 diese Lücke durch die Sequenced Collections, genauer die Einführung der Interfaces SequencedCollection, SequencedSet und SequencedMap. Sie bieten Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende der Collection sowie zur Bereitstellung einer Collection in umgekehrter Reihenfolge.

Das Interface SequencedCollection

Betrachten wir das Interface SequencedCollection (Listing 9).

interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();
  void addFirst(E);
  void addLast(E);
  E getFirst();
  E getLast();
  E removeFirst();
  E removeLast();
}

Es ist leicht ersichtlich, dass SequencedCollection das Interface Collection erweitert und Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende bietet. Die Methoden addXyz() und removeXyz() lösen für Immutable Collections eine UnsupportedOperationException aus. Die Methode reversed() ermöglicht die Verarbeitung der Elemente in umgekehrter Reihenfolge. Tatsächlich handelt es sich dabei um eine View, ähnlich wie bei subList(). Dadurch werden Änderungen in der View auch in der Original-Collection sichtbar und vice versa.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Ein kurzer Blick hinter die Kulissen zeigt, wie es mit Hilfe von DefaultMethoden möglich wurde, die Interfaces in die bestehende Interfacehierarchie einzupassen (Listing 10).

public interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();

  default void addFirst(E e) {
    throw new UnsupportedOperationException();
  }

  default void addLast(E e) {
    throw new UnsupportedOperationException();
    }

  default E getFirst() {
    return this.iterator().next();
  }

  default E getLast() {
    return this.reversed().iterator().next();
  }

  default E removeFirst() {
    var it = this.iterator();
    E e = it.next();
    it.remove();
    return e;
  }

  default E removeLast() {
    var it = this.reversed().iterator();
    E e = it.next();
    it.remove();
    return e;
  }
}

Mit dieser Umsetzung sehen wir die Interfacehierarchie und die drei grün markierten neuen Interfaces der Sequenced Collections in Abbildung 2.

Abb. 2: Die Interfacehierarchie (neue Interfaces sind grün markiert) (Bildquelle: [1])

Die Interfaces SequencedSet und SequencedMap

Betrachten wir nun noch die beiden Interfaces SequencedSet und SequencedMap. Beginnen wir mit SequencedSet. Es erweitert Set und basiert auch auf SequencedCollection, allerdings ohne neue Methoden zu definieren und mit einer kleinen Abweichung bei reversed(), das eine kovariante Überschreibung mit geändertem Rückgabewert ist:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
  SequencedSet<E> reversed(); // covariant override
}

Analog zu SequencedCollection bietet SequencedMap die folgenden Methoden:

  • Entry<K, V> firstEntry() – liefert das erste Schlüssel-Wert-Paar
  • Entry<K, V> lastEntry() – liefert das letzte Schlüssel-Wert-Paar
  • Entry<K, V> pollFirstEntry() – entfernt das erste Schlüssel-Wert-Paar und gibt es zurück
  • Entry<K, V> pollLastEntry() – entfernt das letzte Schlüssel-Wert-Paar und gibt es zurück
  • V putFirst(K, V) – fügt ein Schlüssel-Wert-Paar am Anfang ein
  • V putLast(K, V) – fügt ein Schlüssel-Wert-Paar am Ende an
  • SequencedMap<K, V> reversed() – gibt eine View in umgekehrter Reihenfolge zurück

Damit ergibt sich die Interfacedefinition aus Listing 11.

interface SequencedMap<K,V> extends Map<K,V> {
  SequencedMap<K,V> reversed();
  SequencedSet<K> sequencedKeySet();
  SequencedCollection<V> sequencedValues();
  SequencedSet<Entry<K,V>> sequencedEntrySet();
  V putFirst(K, V);
  V putLast(K, V);
  Entry<K, V> firstEntry();
  Entry<K, V> lastEntry();
  Entry<K, V> pollFirstEntry();
  Entry<K, V> pollLastEntry();
}

SequencedMap-API

Das API von SequencedMap fügt sich nicht so gut in die Sequenced Collections ein. Es verwendet NavigableMap als Basis, daher bietet es statt getFirstEntry() die Methode firstEntry() und statt removeLastEntry() bietet es pollLastEntry(). Diese Namen korrespondieren nicht mit denen der SequencedCollection. Allerdings hätte der Versuch, dies zu tun, dazu geführt, dass NavigableMap vier neue Methoden erhalten hätte, die das Gleiche tun, wie die vier anderen Methoden, über die es bereits verfügt.

Sequenced Collections in Aktion

Zum Abschluss wollen wir die neuen Möglichkeiten einmal in Aktion erleben. Dazu definieren wir eine Liste mit Buchstaben und fragen im Anschluss das erste und das letzte Element ab. Danach erzeugen wir mit reversed() eine Collection mit umgekehrter Reihenfolge, die wir durchlaufen, zur Demonstration in einen Stream wandeln und die drei Elemente überspringen sowie schließlich das erste und letzte Element der umgekehrten Reihenfolge abfragen (Listing 12). Die Ausgabe zeigt Listing 13.

public static void sequenceCollectionExample() {
    System.out.println("Processing letterSequence with list");
    SequencedCollection<String> letterSequence = List.of("A", "B", "C", 
                                                         "D", "E");
    System.out.println(letterSequence.getFirst() + " / " +
                       letterSequence.getLast());

    System.out.println("Processing letterSequence in reverse order");
    SequencedCollection<String> reversed = letterSequence.reversed();
    reversed.forEach(System.out::print);
    System.out.println();
    System.out.println("reverse order stream skip 3");
    reversed.stream().skip(3).forEach(System.out::print);
    System.out.println();
    System.out.println(reversed.getFirst() + " / " + 
                       reversed.getLast());
    System.out.println();
}
Processing letterSequence with list
A / E
Processing letterSequence in reverse order
EDCBA
reverse order stream skip 3
BA
E / A

 

Variieren wir nun die Datenstruktur und nutzen Sets zur Verwaltung der Elemente. Zunächst einmal wird durch mehrmaliges Ausführen deutlich, dass die mit Set.of() erzeugten Daten keine fixe Reihenfolge besitzen, sondern dass diese von Ausführung zu Ausführung variiert. Am Beispiel eines TreeSet schauen wir uns die Möglichkeiten der Sequenced Collections, genauer des SequencedSet, an (Listing 14). Die Ausgabe zeigt Listing 15.

public static void sequencedSetExample() {
    // plain Sets do not have encounter order ... 
    // run multiple times to see variation
    System.out.println("Processing set of letters A-D");
    Set.of("A", "B", "C", "D").forEach(System.out::print);
    System.out.println();
    System.out.println("Processing set of letters A-I");
    Set.of("A", "B", "C", "D", "E", "F", "G", "H", "I").
        forEach(System.out::print);
    System.out.println();

    // TreeSet has order
    System.out.println("Processing letterSequence with tree set");
    SequencedSet<String> sortedLetters = 
                         new TreeSet<>((Set.of("C", "B", "A", "D")));
    System.out.println(sortedLetters.getFirst() + " / " + 
                       sortedLetters.getLast());
    sortedLetters.reversed().forEach(System.out::print);
    System.out.println();
}
Processing set of letters A-D
DCBA
Processing set of letters A-I
IHGFEDCBA
Processing letterSequence with tree set
A / D
DCBA

Fazit

Wir haben uns exemplarisch mit den Sequenced Collections als finalem Feature und mit den String Templates als vielversprechendem Preview Feature beschäftigt.

Sequenced Collections bereichern die ohnehin schon ausgefeilte Funktionalität des Collections-Frameworks und vereinheitlichen vor allem Zugriffe und Veränderungen des ersten und des letzten Elements sowie das Bereitstellen einer umgekehrten Reihenfolge.

Für die Konkatenation von fixen und variablen Textbestandteilen zu einem String gab es bisher schon diverse Varianten, alle mit spezifischen Stärken und Schwächen. String Templates werden die Art und Weise, wie man diese Aufgabe bewältigt, revolutionieren und stellen ein Feature dar, das Java wieder ein wenig moderner macht und zu anderen Sprachen wie Python oder Kotlin aufschließen lässt.

Java 21 bringt viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank Unnamed Classes und Instance Main Methods lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen und für Anfänger schwierigen Begrifflichkeiten erstellen.

In diesem Sinne: Happy Coding mit dem brandaktuellen Java 21!


Links & Literatur

[1] https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png

The post Neues in Java 21: JEP 430 und 431 appeared first on JAX.

]]>
Wo steht Java? https://jax.de/blog/wo-steht-java-nachrichten-aus-einem-bewegten-oekosystem/ Tue, 11 Jul 2023 10:17:01 +0000 https://jax.de/?p=88795 Der „2023 State of the Java Ecosystem“-Report von New Relic zeigt interessante Entwicklungen im Java-Umfeld: So hat zum Beispiel Oracle seine Marktdominanz in den letzten Jahren verspielt, Container gehören unter Java zum Standard und Java 17 stellt – gemessen an seiner Beliebtheit – alle in den Schatten. Ein Überblick über den aktuellen Stand und wichtige Ereignisse in der Geschichte von Java.

The post Wo steht Java? appeared first on JAX.

]]>
Sun Microsystems veröffentlichte Java im Jahr 1996, um eine portablere und interaktivere Methode zur Entwicklung moderner Multimediaanwendungen zu bieten. Es entstand die erste plattformunabhängige, objektorientierte Programmiersprache. Bis heute ist sie überaus populär: Java wird in fast allen wichtigen Industrie- und Wirtschaftszweigen eingesetzt, bietet Tausende von Bibliotheken und wird gut unterstützt. Sicherheitsaspekte und Parallelverarbeitung (Threads) spielten von Beginn an eine wichtige Rolle.

Ab 2006 veröffentlichte Sun Teile von Java, wie etwa den Compiler javac und die Hotspot Virtual Machine, sowie später große Teile des Java-SE-Quellcodes zur Erstellung eines JDK (Java Developer Kits) unter der GPU General Public License – ein wichtiger Impuls für die Open-Source-Bewegung. Zugleich rief das Unternehmen die OpenJDK-Community ins Leben, die auf der offiziellen, frei verfügbaren Implementierung der Java-Plattform beruht. Mit der Übernahme von Sun durch Oracle im Jahr 2010 änderte sich zunächst nichts an der Open-Source-Lizenzierung. Oracle investierte weiterhin in die Programmiersprache und in ein umfassendes Ökosystem.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Oracles Marktdominanz ist Geschichte

Indes lassen sich interessante – Oracle-Kritiker:innen nennen es vorhersehbare – Entwicklungen am Markt beobachten. Schon in dem „State of the Java Ecosystem“-Report aus dem Jahr 2020 [1] konstatierte Herausgeber New Relic einen Rückgang von Oracles Dominanz auf dem Java-Markt. New Relic wertet für die Studie Daten von Entwickler:innen aus, die die Observability-Plattform des Unternehmens nutzen, um ihre Softwareanwendungen zu monitoren und zu debuggen. Anonymisierte und verallgemeinerte Daten mehrerer Millionen VMs helfen so, einen Überblick über die Entwicklungen des Java Ökosystems zu erhalten.

Demnach verfügte Oracle im Jahr 2020 über einen Marktanteil von etwa 75 Prozent, Tendenz bereits sinkend. Als zweitbeliebtester Anbieter hatte sich das von der Community geführte AdoptOpenJDK etabliert. Auffällig dabei: Während zu dem Zeitpunkt erst etwa elf Prozent der Nutzer:innen auf die damals neue Version Java 11 migriert waren, liefen bereits gut ein Drittel der AdoptOpenJDK VMs unter Java 11. Das lässt den Schluss zu, dass Anwender:innen die Migration nutzten, um zugleich den Distributor zu wechseln. Der wahrscheinlichste Grund: Oracle hatte eine neue Lizenzierungspolitik eingeführt. Zwar blieb die im Januar 2019 veröffentlichte Oracle Java 8 SE kostenfrei. Für den Support und die Nutzung von Updates sowie Security Patches benötigten kommerzielle Anwender:innen allerdings nun eine kostenpflichtige Lizenz.

Die Kund:innen reagierten verärgert. Dass viele sich gegen ein Lizenzabonnement und für eine Alternative entschieden, wird besonders im aktuellen „State of the Java Ecosystem“-Report [2] deutlich. So sank der Marktanteil von Oracle bis zum Jahr 2022 auf 34 Prozent, aktuell beziehen nur noch etwa 28 Prozent ihr JDK von dem einstigen Marktführer. Diese Position hat nun Amazon mit einem Anteil von etwa 31 Prozent übernommen. Sicher profitiert Amazon dabei von dem seit vielen Jahren stetig wachsenden AWS-Geschäft und der großen Community drumherum. Java Coretto ist eine plattformübergreifende Distribution, die auf OpenJDK basiert und damit auch auf dessen Lizenzierungsrichtlinien (GNU General Public License). Die Auslieferung der neuen Releases geht dabei mit den LTS-Releases von OpenJDK Hand in Hand. Die vierteljährlichen Updates enthalten Bugfixes und Performanceoptimierungen.

Inzwischen hat Oracle seine Lizenzpolitik modifiziert: Seit Java 17, dem bisher letzten LTS-Release, kehrte der Anbieter (beinahe) zu seinem kostenlosen Angebot zurück. Nutzer:innen müssen nun den „No Fee Terms and Conditions“ (NFTC) zustimmen und erhalten dafür kostenlose Updates bis Herbst 2024. Das offizielle Supportende ist allerdings für 2026 terminiert. Wer auch nach 2024 von kostenlosen Updates und Patches profitieren möchte, muss entweder für den Support zahlen oder zügig auf die im Herbst 2023 erscheinende Java-Version 21 LTS migrieren. Nicht nur Amazon weiß indes den Oracle-Lizenz-Wirrwarr für sich zu nutzen. So bauten beispielsweise auch die JDK-Distributoren Eclipse Adoptium und RedHat ihre Marktanteile aus (auf mehr als zwölf und 10,5 Prozent) und sind damit keineswegs nur Nischenanbieter.

LTS für die Produktion, Nicht-LTS zum Experimentieren

Der Wechsel zu einem anderen Distributor steht vor allem dann im Raum, wenn ohnehin auf eine neue Java-Version migriert werden muss oder soll. Seit der Übernahme von Sun Microsystems durch Oracle ließen die neuen Releases zunächst jeweils mehr als drei Jahre auf sich warten. Zwischen dem Erscheinen der Version 8, die, wenn auch nicht offiziell so bezeichnet, als Long-Term-Support-Version gelten kann, und dem Release 11 war lange nicht klar, wann das nächste Java LTS erscheinen würde. Nach Java 9 switchte Oracle seinen Releasezyklus und veröffentlichte fortan halbjährlich neue Major Releases. Was mehr Übersicht und Planungssicherheit bringen sollte, stieß auf ein geteiltes Echo. Befürworter:innen freuten sich über kürzere Wartezeiten auf neue Features und Bugfixes. Kritiker bemängelten die kurzen Supportzeiten der Nicht-LTS-Versionen und zweifelten, ob das ganze Umfeld aus IDEs und Konnektoren hier mithalten könne.

Folglich werden vor allem LTS-Versionen (Java 8, Java 11 und Java 17) in der operativen Anwendungsentwicklung eingesetzt. Die Nicht-LTS-Versionen eignen sich hingegen, um zwischendurch neue Features zu testen und die nächste LTS-Migration zu planen. Diese verständliche Zurückhaltung in produktiven Systemen erklärt, warum noch im Jahr 2020 fast 85 Prozent der Java VMs auf Java 8 liefen (laut der Datenerhebung von New Relic), obwohl Java 11 LTS bereits zwei Jahre erhältlich war. Erst etwa elf Prozent nutzen zu diesem Zeitpunkt bereits Java 11.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Dennoch warteten viele Development-Teams nicht bis zum Launch der aktuellen Java-LTS-Version 17, sondern migrierten ihre VMs auf Java 11: Aktuell (im Jahr 2023) laufen mehr als 56 Prozent der Java VMs unter Java 11, knapp ein Drittel ist bisher noch auf Java 8 verblieben. Java 17 scheint sich aber nun dennoch durchzusetzen: Während es mehrere Jahre gedauert hat, bis Java 11 eine entsprechende Akzeptanz erreichte, vervielfachte Java 17 seinen Anteil innerhalb eines Jahres. Basierten im vergangenen Jahr nur etwa ein Prozent der von New Relic ausgewerteten Installationen auf Java 17, waren es im Frühling dieses Jahres bereits neun Prozent. Die Nicht-LTS-Versionen spielen indes die erwähnte Nebenrolle: Die beliebtesten unter ihnen sind Java 14 (knapp 0,6 Prozent Anteil) und Java 15 (gut 0,4 Prozent). Noch weniger Installationen verzeichnen die Versionen 16, 18 und 19.

Entwicklung und Migration: von Frameworks und Open-Source-Managern

Ist die Migration auf eine neue Java-Version aufwendig? Auf diese Frage gibt es keine einfache Antwort, denn viele Faktoren spielen eine Rolle. Grundsätzlich ist hierbei die Plattformunabhängigkeit von Java ein großer Vorteil. Erscheint eine neue Version, werden die IDEs (Integrated Development Environments) zügig angepasst. So werden zum Beispiel bei den meisten Distributionen mit der neuen Version die modifizierten Compiler etwa für die verschiedenen Gerätebetriebssysteme ausgeliefert, sodass die Anwendungen auch mit der neuen Java-Version auf den bisherigen Endgeräten laufen. Dennoch reicht eine einfache Neukompilierung der Anwendungen normalerweise nicht aus. Oft muss der Code angepasst werden, vor allem dann, wenn Drittanwendungen oder Funktionen benutzt werden.

Zudem ist es nicht ganz einfach, die verschiedenen genutzten Frameworks und Java Services im Auge zu behalten. Frameworks haben sich aus den Communities heraus entwickelt – aus Ansammlungen vorgefertigter Codeabschnitte sind ganze Java-Plattformen mit Library-Sammlungen von vordefinierten Klassen und Funktionen geworden –, Entwickler:innen haben die Qual der Wahl. Marktführend ist hier das Spring Boot Framework von VMware, das auf dem vor gut 20 Jahren entwickelten Open Source Framework Spring beruht. Spring Boot liegt das Designprinzip „Konvention vor Konfiguration“ zugrunde. Das bedeutet, dass vorgefertigte Funktionen bereits so vorkonfiguriert sind, dass eigene lauffähige Anwendungen recht einfach entwickelt werden können. So vereinfacht das Framework nicht nur die Entwicklung von Microservices, sondern auch komplexerer Softwaremodule.

Im Zuge technologischer Trends wie Containerisierung und Cloud Computing sind in den letzten Jahren Frameworks entstanden, die mit erweiterten Basiskonzepten arbeiten. SpringBoot gilt als reaktives Framework – auch wenn die Definitionen, was genau unter Reaktivität zu verstehen ist, teils sehr weit auseinandergehen. Gemeinhin meint reaktiv, dass asynchrone Datenströme die Programmierung bestimmen und die Anwendung schließlich auf Ereignisse im Datenstrom reagiert. Frameworks, wie zum Beispiel Quarkus, erfreuen sich wachsender Beliebtheit: Das containernative Framework, das für die Ausführung in einer Kubernetes-Umgebung konzipiert ist, kombiniert imperative (also klassische, ablauforientierte) und reaktive Programmierprinzipien. Der Fokus liegt auf leistungsbezogenen Funktionen, was es interessant für die Entwicklung von Microservices und Cloud-Infrastrukturen macht.

Viele Developer:innen nutzen gleich mehrere Frameworks, um bei der Erstellung der eigenen Anwendungen Zeit zu sparen. Das ist nicht ganz unkompliziert, haben doch vor allem größere Frameworks definierte Regeln und interne Abhängigkeiten. Letztere fließen in den eigenen Java-Code ein, Codeabschnitte verschiedener Frameworks sollten nicht durcheinandergeraten, und die Codes und Bibliotheken müssen auf allen Plattformen gepflegt werden. In manchen, vor allem größeren Unternehmen findet man inzwischen einen dedizierten Open-Source-Manager, der sich um diese Fragen kümmert.

Anwendungen im Containerformat

Die Entwicklungen in Java spiegeln auch die allgemeinen Development-Trends wider. Bereits Java 9 führte einige Funktionen ein, um die Arbeit mit Containern zu verbessern. Werden Anwendungen mit Hilfe von Containertechnologien entwickelt, entsteht eine Kapsel, die alle notwendigen Services für den Betrieb der Anwendung enthält. Damit werden Anwendungen unabhängiger von Betriebssystemen oder Infrastrukturen ausführbar. Liegen dem Betrieb Plattformen wie Kubernetes oder Amazon ECS zugrunde, sind sowieso Container-Images notwendig. Heute wird die Mehrzahl der Java-Anwendungen – etwa 70 Prozent – in Form von Container-Images ausgeliefert.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Eine typische Funktion, welche die Arbeit mit Containern erleichtert, ist die Möglichkeit, die Startflag -XX:MaxRAMPercentage zu setzen. Sie ersetzt die genaue Definition einer maximalen Heap-Größe, also der Speicherkapazität, die Java für die Anwendungsausführung reservieren soll. Eigentlich ist die Java Virtual Machine (JVM) als Teil der Java-Runtime-Umgebung dafür zuständig, dass eine Java-Applikation portabel ist und ihr genügend Speicher zur Verfügung steht. Weil Anwendungen immer um begrenzte Ressourcen konkurrieren, prüft die JVM zunächst, welche Ressourcen zur Verfügung stehen, und konfiguriert dann die Java-Anwendungen möglichst optimal. Diese sogenannten Ergonomics sind automatisierte Standards, die auch manuell eingestellt werden können.

Doch containerbasierte Anwendungen funktionieren anders: Es sind kleinere, gekapselte Applikationen, denen alle Ressourcen innerhalb des Containers zur Verfügung stehen. Standardeinstellungen wie etwa die Heap-Größe, die die JVM wählt, um Ressourcen zu schonen und zu teilen, erweisen sich nun als unpassend. Häufig bleiben dabei sogar Ressourcen ungenutzt, weil die JVM beispielsweise nur einen Teil des Heap-Speichers zuweist, obwohl innerhalb des Containers ein wesentlich größerer Anteil sinnvoll wäre. Die Startflag löst das Problem, indem sich nun ein prozentualer Anteil an Speicher zuweisen lässt. Das ist deutlich flexibler und nutzt den vorhandenen Speicher containeroptimiert besser aus.

Immer mehr Entwickler:innen nutzen zudem Multi-Core-Konfigurationen, auch dann, wenn sie containerbasiert entwickeln. Noch läuft mehr als ein Drittel und damit die Mehrheit der Java-Anwendungen mit nur einem Core. Doch die Tendenz ist rückläufig – vor einem Jahr waren es noch mehr als 40 Prozent. Fast genauso viele Anwendungen (knapp 30 Prozent) werden inzwischen entwickelt, um auf acht Cores optimale Performance zu erzielen – ein Anstieg um fast 50 Prozent. Die Arbeit mit Threads – und damit auch das Multithreading, also das parallele Abarbeiten von Prozessen – gehörte von Beginn an zu den überzeugenden Argumenten von Java als Programmiersprache. Doch erst seit Java 8 waren die Fortschritte in diesem Bereich so maßgeblich, dass die Entwicklung von Anwendungen für Multi-Core-Umgebungen praktikabel wurde. Bei nicht containerbasierten Java-Anwendungen ist diese Praktik entsprechend weiter verbreitet.

Garbage Collectoren: von Parallel bis G1

Denkt man über das Thema Speicheroptimierung nach, ist auch das Thema Garbage Collector (GC) von entscheidender Bedeutung. Genau wie die Heap-Konfiguration gehört die Wahl des GC zu den Ergonomics der JVM. Die automatische Garbage Collection ist ein Prozess, der den Heap-Speicher auf nicht verwendete Objekte untersucht, die dann gelöscht werden. So wird zwar wieder Speicher für die Anwendung frei, allerdings verbraucht auch der Prozess selbst Ressourcen – und zwar nicht zu knapp. Die Wahl des richtigen Algorithmus ist deshalb wichtig.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

In den Anfangszeiten von Java gab es zunächst nur einen ParallelGC: Die Java-Anwendung musste gestoppt werden, damit der GC die unnützen Objekte ausfindig machen konnte. Alle Anfragen, die während dieser Unterbrechung an die Anwendung gestellt wurden, mussten warten, bis der GC seine Arbeit beendet hatte. Je nach definierter Heap-Größe variierte die Dauer der Unterbrechung. Bis Java 8 war der ParallelGC die Standardform der Müllbeseitigung. Der ParallelGC nutzt mehrere Threads gleichzeitig, was wiederum Overhead erzeugt. Das Pendant, der SerialGC, arbeitet im Prinzip gleich, nutzt aber nur einen Thread. Für clientartige Single-Prozessor-Umgebungen eignet er sich daher besser.

Mit der Java Version 1.2 kam ein GC hinzu, der ein anderes Prinzip nutzt: der Concurrent Mark Sweep GC, kurz CMS. Er arbeitet, zumindest teilweise, „concurrent“ (also gleichzeitig) zur Anwendung. Das hat Vor- und Nachteile: Zum einen werden die notwendigen Pausen deutlich kürzer, zum anderen wird aber die Anwendung langsamer, während der CMS arbeitet. Wahrscheinlich war das der Grund, warum sich nie eine Mehrheit für den CMS entschied, der in Java 8 in seiner letzten Version erschien. Mehr als ein Drittel der JVMs, die unter Java 10 oder einer älteren Version laufen, vertrauen die Aufräumarbeiten dem SerialGC an, knapp 22 Prozent dem CMS und weitere 20 Prozent greifen auf den ParalelGC zurück.

Der Nachfolger des CMS, der Garbage First Garbage Collector (G1), begann seinen Siegeszug ab Version 9. Auch er arbeitet teils concurrent zur Anwendung und teils in Stop-the-World-Pausen. Aber der G1 beruht auf einer anderen Implementierung und beseitigt ein gar nicht so seltenes Problem des CMS: Unter bestimmten Umständen, etwa wenn die Heaps stark fragmentiert sind oder der GC zu viel Zeit für seine Arbeit benötigt, wird eine Full Collection ausgelöst, die dann die Anwendung doch wieder unbefriedigend lange lahmlegt. Das Tuning des CMS, mit dem sich das verhindern ließe, ist kompliziert. Statt größere Regionen auf einmal zu löschen, unterteilt der G1 sein Arbeitsgebiet in kleinere Regionen. Das optimiert den Sammelprozess, und die Anwendung friert seltener ein. Außerdem lässt sich vergleichsweise unkompliziert im Vorfeld festlegen, wie die Aufteilung der CPU-Ressourcen zwischen Anwendung und Müllbeseitigung aussehen soll. Im Zweifel erhält die Anwendung so zumindest immer die definierten Ressourcen.

Dass der G1 derzeit wahrscheinlich der beste Kompromiss zwischen Latency und Throughput ist, zeigen die Nutzerzahlen: Fast zwei Drittel derer, die ihre VMs mit Java 11 oder einer höheren Version betreiben, nutzen den G1. 28 Prozent vertrauen nach wie vor dem SerialGC, und die Anteile von CMS und ParallelGC gehen gegen null. Andere, eher als experimentell zu bezeichnende Garbage Collectors, wie zum Beispiel ZGC und Shenandoah, kommen in Produktionssystemen noch immer kaum zum Einsatz. Beide sind allerdings auch noch jünger und fokussieren sich auf niedrige Pausenzeiten, was in Anwendungsfällen von G1 nicht unbedingt Priorität war. Daher sind beide produktionsreife Versionen eher wenig in Nutzung.

Die Zukunft von Java

Für den Herbst ist der Launch der neuen Java-Version 21 LTS geplant, die Supportunterstützung ist bis 2028, der erweiterte Support bis 2031 terminiert. Sie wird einige verbesserte und neue Features, wie etwa virtuelle (leichtgewichtige) Threads, Verbesserungen im Pattern Matching und String Templates, enthalten. Das Java-Ökosystem wird sich entsprechend weiterentwickeln, mittelfristig ist der Entwicklungspfad damit gesichert.

 

Ob Java eine weit verbreitete Programmiersprache bleibt? Andere Sprachen sind auf dem Vormarsch: So machte beispielsweise Google schon 2017 Kotlin zur zweiten offiziellen Android-Programmiersprache neben Java. Ursprünglich für die JVM entwickelt, bringt die junge Sprache einige interessante Features mit; bei der Programmierung kommen Entwickler:innen mit weniger Code aus. Kotlin ist kompatibel zu Java und kann – durch Übersetzung des Codes in JavaScript – auch fürs Web genutzt werden. Auch das leicht zu erlernende Python ist immer wieder als Java-Alternative im Gespräch. Plattformunabhängige, objektorientierte oder prozedurale Programmierung und schnelle Launches machen Python beliebt. Außerdem steht die Frage im Raum, wie Oracle mit den schrumpfenden Marktanteilen umgehen und welcher Entwicklungsaufwand künftig in Java fließen wird.

Allerdings steht Java keineswegs auf wackeligen Füßen. Beinahe alle großen Tech-Unternehmen nutzen die Sprache ausgiebig selbst und Entwickler:innen können unter zahlreichen Distributionen, JDKs, Drittanwendungen und Supportangeboten wählen. Die Community ist groß und sehr aktiv – zahllose verschiedene Frameworks, Tools, Libraries und ähnliche Werkzeuge unterstützen bei der Entwicklungsarbeit. Auch wenn andere Programmiersprachen hier aufholen oder sich für bestimmte Anwendungsfälle besser eignen – an Java vorbeikommen wird man vorerst nicht.

Die statistischen Zahlen in diesem Beitrag beruhen auf dem Report „2023 State of the Java Ecosystem – an in-depth look at one of the most popular programming languages“ [2].


Links & Literatur

[1] https://newrelic.com/blog/nerd-life/state-of-java

[2] https://newrelic.com/resources/report/2023-state-of-the-java-ecosystem

The post Wo steht Java? appeared first on JAX.

]]>