Der 16. September markiert einen wichtigen Meilenstein im Java-Ökosystem: Die Veröffentlichung der neuesten LTS-Version – Java 25. Es sind fast genau zwei Jahre seit dem letzten LTS-Release Java 21 (19. September 2023) vergangen, und diese Version mit ihren 18 JEPs enthält spannende Neuerungen sowohl für Einsteiger als auch für erfahrene Entwickler.

API-Änderungen

JEP 506: Scoped Values

Link zu JEP 506

Scoped Values ermöglichen es einer Methode, unveränderliche Daten sowohl mit ihren aufgerufenen Methoden innerhalb eines Threads als auch mit untergeordneten Threads zu teilen. Sie sind vergleichbar mit ThreadLocal, das in Java 1.2 eingeführt wurde, wurden jedoch im Hinblick auf Benutzerfreundlichkeit, Verständlichkeit, Robustheit und Performance entwickelt.

Einfach ausgedrückt erlaubt es Scoped Values, dass verschiedene Teile eines Java-Programms einen Zustand gemeinsam nutzen können, ohne dass dieser explizit als Eingabeparameter jeder Methode übergeben werden muss. Das kann besonders nützlich in Webanwendungen sein, bei denen ein Kontext zwar von einem Request-Handler aus einer Anfrage abgeleitet wird, aber von jeder Methode verwendet werden kann, die diesen Kontext benötigt – auch wenn sie selbst keinen Zugriff auf das ursprüngliche Request hat.

Dies lässt sich im folgenden Beispiel demonstrieren:

class Framework {

    private static final ScopedValue<FrameworkContext> CONTEXT
                        = ScopedValue.newInstance();    

    void serve(Request request, Response response) {
        var context = createContext(request);
        where(CONTEXT, context)                         
                   .run(() -> Application.handle(request, response));
    }
    
    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();                    
        var db = getDBConnection(context);
        db.readKey(key);
    }

}

Hier richtet ScopedValue#where die Umgebung ein, und #run führt Application#handle mit diesem Kontext aus. Der Kontext wird unmittelbar nach dem Verlassen der Methode wieder gelöscht.

JEP 510: Key Derivation Function API

Link zu JEP 510

A KDF (Key Derivation Function) is often used to create cryptographic data from which multiple keys can be obtained. …Deriving keys is similar to hashing passwords.

Diese neue API, die sich in der Vorschau in JEP 478 befand und in dieser Version finalisiert wird, bringt eine einheitliche Möglichkeit für Java, KDF-Algorithmen zu handhaben – zum Beispiel durch die Erzeugung kryptografisch sicherer Schlüssel, wie im folgenden Beispiel:

// Ein KDF-Objekt für den Algorithmus HKDF-SHA256 erzeugen
KDF hkdf = KDF.getInstance("HKDF-SHA256"); 

// Eine Parameter-Spezifikation von Typ ExtractExpand erzeugen
AlgorithmParameterSpec params =
    HKDFParameterSpec.ofExtract()
                     .addIKM(initialKeyMaterial)
                     .addSalt(salt).thenExpand(info, 32);

// Den 32-byte AES Schlüssel ermitteln
SecretKey key = hkdf.deriveKey("AES", params);

// Weitere Schlüsselermittlungen können mit demselben KDF-Objekt durchgeführt werden.

Wenn Sie es gewohnt sind, die alten APIs wie KeyGenerator und SecretKeyFactory zu verwenden, werden Sie sicher zustimmen, dass die neue Vorgehensweise deutlich eleganter und prägnanter ist.

JEP 511: Module Import Declarations

Link zu JEP 511

Dieses JEP zielt darauf ab, die Gesamtanzahl der Import-Anweisungen am Anfang jeder Java-Datei zu reduzieren, indem es ermöglicht wird, ganze Module statt einzelner Klassen zu importieren – ähnlich wie es bereits mit Klassen und Interfaces im Paket java.lang gehandhabt wird. Anstatt also etwas wie Folgendes schreiben zu müssen:

import java.util.Map;                   
import java.util.function.Function;     
import java.util.stream.Collectors;    
import java.util.stream.Stream;         

kann dasselbe jetzt mit einer einzigen Zeile erreicht werden:

import module java.base;

Falls es zu Mehrdeutigkeiten bei der importierten Klasse oder dem Interface kommt (etwa weil verschiedene Pakete Klassen oder Interfaces mit demselben Namen enthalten), kann dies durch den expliziten Import des vollständig qualifizierten Namens aufgelöst werden, z. B.:

import module java.base;      // exportiert java.util, das eine public Date-Klasse enthält
import module java.sql;       // exportiert java.sql, as eine public Date-Klasse enthält

import java.sql.Date;         // behebt die Mehrdeutigkeit des einfachen Namens "Date"!

...
Date d = ...                  // Alles gut! Date wird zu java.sql.Date aufgelöst.

JEP 512: Compact Source Files and Instance Main Methods

Link zu JEP 512

Diese Änderung richtet sich in erster Linie an Anfänger, indem sie Boilerplate-Code reduziert – besonders an Stellen, an denen Neueinsteiger ihren ersten Code schreiben, ohne bereits alle Bestandteile der Java-Sprache zu verstehen. Das wird im folgenden Beispiel deutlich. Hier ist das klassische “Hallo Welt”-Beispiel, dem jeder Anfänger begegnet:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hallo, Welt!");
    }
}

Und so lässt sich dieselbe Funktionalität nun auf neue Weise erreichen:

void main() {
    IO.println("Hallo, Welt!");
}

Es ist definitiv viel einfacher und unkomplizierter für eine Lehrkraft, einem Anfänger zu erklären, wie es funktioniert. Natürlich kann die neue IO-Klasse nicht nur zum Ausgeben auf der Konsole verwendet werden, sondern auch zum Einlesen! Zum Beispiel:

void main() {
    String name = IO.readln("Bitte geben Sie Ihren Namen ein: ");
    IO.print("Schön Sie kennenzulernen, ");
    IO.println(name);
}

JEP 513: Flexible Constructor Bodies

Link zu JEP 513

Flexible Constructor Bodies vereinfachen die Initialisierungslogik von Objekten in Java, indem sie es erlauben, Anweisungen vor einem expliziten Konstruktoraufruf, also super() oder this(), zu platzieren. Dadurch wird eine einfache Möglichkeit geschaffen, Argumente zu validieren, bevor die Konstruktoren der Superklasse aufgerufen werden, und insgesamt zusätzliche Sicherheiten geboten, dass der Zustand eines neuen Objekts vollständig initialisiert ist, bevor es bei anderen Stellen des Programms verwenden kann. Hier ist ein einfaches Beispiel, das zeigt, was nun möglich ist:

class Employee extends Person {

    String officeID;

    Employee(..., int age, String officeID) {
        if (age < 18  || age > 67)
            // Now fails fast!
            throw new IllegalArgumentException(...);
        super(..., age);
        this.officeID = officeID;
    }

}

Änderungen der Java-Platform

JEP 503: Remove the 32-bit x86 Port

Link zu JEP 503

Mit der Deprecation des 32-Bit-x86-Ports in Java 24 durch JEP 501 wird nun auch der Quellcode entfernt. Dies betrifft den Linux 32-Bit-x86-Port, der als letzter im JDK verbleibt. Ziel ist es, die Build- und Test-Infrastruktur des JDK zu vereinfachen und neue Features zu ermöglichen, die plattformspezifische Unterstützung benötigen, ohne auf 32-Bit-x86-Fallbacks Rücksicht nehmen zu müssen.

JEP 519: Compact Object Headers

Link zu JEP 519

Compact Object Headers sind keine experimentelle Funktion mehr. Zusammen mit JEP 450 ist das Ziel, die Größe der Objekt-Header in der HotSpot JVM auf 64-Bit-Architekturen von bisher 96 bis 128 Bit auf 64 Bit zu reduzieren. Dies verringert die Heap-Größe und erhöht die Datenlokalität.

Compact headers bieten eindeutige Leistungsverbesserungen, wie in den folgenden Experimenten gezeigt wird:

JEP 521: Generational Shenandoah

Link zu JEP 521

Der Shenandoah-Garbage-Collector wurde in JEP 404 (Java 24) mit generationalen Sammelfähigkeiten erweitert, um den nachhaltigen Durchsatz, die Belastungsspitzen-Resilienz und die Speichernutzung zu verbessern. Mit Java 25 ist dies keine experimentelle Funktion mehr und kann über die folgenden Kommandozeilenoptionen aktiviert werden:

$ java -XX:+UseShenandoahGC \
       -XX:ShenandoahGCMode=generational ...

AOT-Optimierungen

JEP 514: Ahead-of-Time Command-Line Ergonomics

Link zu JEP 514

Um das Erstellen von ahead-of-time caches, die den Start von Java-Anwendungen beschleunigen, zu erleichtern, wurden die für gängige Anwendungsfälle erforderlichen Kommandozeilenargumente vereinfacht. Was zuvor ein zweistufiger Workflow war:

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
       -cp app.jar com.example.App ...

$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
       -XX:AOTCache=app.aot

kann jetzt in nur einem, kürzeren Befehl erledigt werden:

$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

JEP 515: Ahead-of-Time Method Profiling

Link zu JEP 515

Weitere Optimierungen wurden für die AOT-Kompilierung der HotSpot JVM implementiert. Mit dieser Änderung ist der JIT-Compiler in der Lage, nativen Code sofort beim Anwendungsstart zu generieren, anstatt darauf warten zu müssen, dass Profile gesammelt werden. Diese Optimierungen betreffen den in JEP 483 eingeführten AOT-Cache, und gemäß der Spezifikation können die Startzeiten bei kleineren Anwendungen um 19 % verbessert werden, während komplexe und länger laufende Programme ebenfalls schneller aufgewärmt werden können.

JFR-Verbesserungen

JEP 518: JFR Cooperative Sampling

Link zu JEP 518

Das Parsen von Thread-Stacks im JDK Flight Recorder kann jetzt noch genauere Ergebnisse liefern. Bisher verwendete JFR Thread-Unterbrechungen zur Datensammlung, was bei hoher Last die Ergebnisse verfälschen konnte. Nun pausieren Threads an safepoints, um Statistiken zu melden. Die Daten bleiben konsistent, und die Auswirkung auf die Performance ist minimal.

JEP 520: JFR Method Timing & Tracing

Link zu JEP 520

Durch die Verwendung von bytecode instrumentation ist der JFR nun in der Lage, Methodentimings und Tracing genauer zu erfassen, ohne sich auf stichprobenbasierte Statistiken zu verlassen.

  • MethodTiming zeichnet die Ausführungszeiten von Methoden mit Nanosekunden-Präzision auf
  • MethodTrace erstellt Aufrufketten für kritische Abschnitte.

Vorschau

Die folgenden Features sind noch nicht finalisiert und können nur mit speziellen Flags in der Kommandozeile aktiviert werden. Da es üblicherweise von JEP zu JEP zu Breaking Changes kommt, werden wir sie nur als Referenzen aufführen, bis die Features stabil sind und in einer zukünftigen Java-Version enthalten sind:

Fazit

Dieser wichtige Meilenstein – die Veröffentlichung der neuesten LTS-Version von Java (Java 25) – bringt spannende Features und Optimierungen in die Programmiersprache und rückt uns gleichzeitig einen Schritt näher an spannende Veränderungen wie die Structured Concurrency, insbesondere nach der Einführung der Virtual Threads in Java 21. In jedem Fall zeigt diese neue Version, dass sich Java kontinuierlich weiterentwickelt und dabei jedes Mal die Werkzeuge, die wir schon lieben, noch besser macht.

Sourcen