Java 25: Die wichtigsten Features seit Version 21
Seit September 2025 gibt es das neue Java 25 LTS-Release. Neben den typischen Performance-Verbesserungen und Ressourcen-Optimierungen fallen vor allem die Vereinfachungen für Java-Neulinge auf. Mehr dazu in diesem Artikel.
Java 25 Features
Als neues LTS-Release bringt Java 25 einige Neuerungen im Vergleich zu Java 21 mit. Unter der Haube wurde viel optimiert, das Web-EntwicklerInnen bei der täglichen Arbeit eventuell gar nicht bemerken. Das sind zum Beispiel Optimierungen bei den Garbage-Kollektoren, der Support-Stop für 32 Bit Rechner oder mehr Unterstützung für die AOT-Kompilierung. In diesem Artikel fokussiere ich mich auf Änderungen, die auffallen. Alles Weitere findet ihr im Detail hier: https://openjdk.org/projects/jdk/25/
Rückblick Java 21 Features
Performance Tuning durch Versionsupdate
Java 25 ist performanter als Java 21. Ich teste es mit einer kleinen, produktiven Spring Boot Webanwendung. Dazu starte ich die Anwendung 10 Mal mit Java 21 und 10 Mal mit Java 25. Dabei protokolliere ich die Startzeiten, wie Spring Boot sie loggt.
- Java 21
- Schnellste Zeit: 2,976 Sekunden
- Langsamste Zeit: 3,376 Sekunden
- Durchschnitt: 3,087 Sekunden
- Java 25
- Schnellste Zeit: 2,537 Sekunden
- Langsamste Zeit: 2,760 Sekunden
- Durchschnitt: 2,653 Sekunden
In diesem einfachen Performancetest ist Java 25 circa 14% schneller als Java 21. Im Vergleich zu älteren Versionen, wie Java 17 oder 11 wäre die Performance-Steigerung noch deutlicher.
Module Import
In Java 25 wurde der Modul-Import eingeführt. Vorher konnte man einzelne Klassen, Konstanten oder Pakete importieren, mit JEP 511 können ganze Module importiert werden:
import module java.base;
public class Java25 {
static void jep511_ModuleImport() {
for (var item : List.of("1", 0))
jep456_UnnamedVariable(item);
}
...
}
Der vorherige Code-Ausschnitt benutzt die Klasse List innerhalb der Klasse Java25. Statt einem Einzelklassen-Import (import java.util.List;) wird sie zusammen mit den anderen Klassen des java.base-Moduls importiert.
Keine Klasse und vereinfachte main-Methode
Java 25 lernt sich leichter für Neulinge, weil:
- die main-Methode kein "public static" und kein "String[] args" mehr benötigt.
- keine Klasse mehr für die main-Methode braucht.
- die Klassen des java.base-Moduls (hier ArrayList und IO) automatisch importiert sind. Wäre die main-Methode in einer Klasse, so müssten wir die Klasse ArrayList importieren.
Hier der Inhalt einer kompletten Java-Datei, welche die neue Klasse IO verwendet, um mit der Konsole zu interagieren:
void main() {
var list = new ArrayList<String>();
while (list.size() < 2) {
IO.print("Enter max 2 items in Console: ");
list.add(IO.readln());
}
IO.println("Java 25 items are " + list);
}
(Mehr dazu in JEP 512.)
Unbenannte Variablen
JEP 456 führt unbenannte Variablen in Java 22 ein. Bisher brauchte jede Variable einen Namen, auch wenn wir sie nicht benutzt haben. Nun können wir ungenutzte Variablen einfach mit Unterstrich _ benennen. Im folgenden Beispiel verwende ich unbenannte Variablen für eine Exception und einen String, die im Code ignoriert werden:
static void jep456_UnnamedVariable(Object obj) {
try {
switch (obj) {
case String _ -> IO.println("Any Text.");
case Integer number -> IO.println(10 / number);
default -> IO.println("Unknown type");
}
} catch (Exception _) {
IO.println("Exception caught without caring about details.");
}
}
Kommentare in Markdown
Seit Java 23 wird Markdown als Format für Kommentare unterstützt. Dazu muss jede Kommentarzeile mit /// starten. Das sieht dann so aus:
/// # JEP 467
/// Java supports comments in Markdown format.
/// Other Java 25 JEPs in this class are:
/// * JEP 511
/// * JEP 456
public class Java25 {
...
Flexible Konstruktor-Bodies
JEP 513 lockert die Regel für Code im Konstruktor. Vor Java 25 durfte vor einem this(...) oder super(...) Konstruktoraufruf kein anderer Code stehen. Durch den flexiblen Konstruktor-Body ist dies nun erlaubt:
public class Java25 {
Java25() {
IO.println("Code before super() is allowed with JEP 513.");
super();
IO.println("Code after super() was allowed before.");
} ...
Stream Gatherers
Gatherers (JEP 485) bereichern die Stream-Verarbeitung seit Java 24. Dazu hat die Stream API eine neue Methode gather, die Elemente im Stream sammelt. gather ist eine intermediate Operation, so dass der Stream nach gather weiter verarbeitet werden kann. Die Grundlagen zu Streams findet ihr hier.
Gatherer ist ein neues Interface, durch dessen Implementierung wir mit der gather Methode one-to-one, one-to-many, many-to-one und many-to-many Mappings im Stream durchführen können. Im folgenden zeige ich 5 Implementierungen, die Teil von Java 25 sind.
fold
Ein many-to-one Mapping im Stream.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Prints 15 (Stream has only 1 element after gather)
numbers.stream()
.gather(Gatherers.fold(() -> 0, Integer::sum))
.forEach(IO::println);
scan
scan nimmt den aktuellen und den vorherigen Wert und wendet eine BiFunction an, um den aktuellen Wert zu ändern.
// Result: [1, 3, 6, 10, 15]
numbers.stream()
.gather(Gatherers.scan(() -> 0, (previous, current) -> previous + current))
.toList();
windowFixed
Sammelt jedes Element in gleichgroßen Unterlisten.
// Result: [[1, 2], [3, 4], [5]]
List<List<Integer>> windows = numbers.stream()
.gather(Gatherers.windowFixed(2))
.toList();
slidingWindow
Ähnlich wie windowFixed nur mit Überlagerungen der Elemente in den gesammelten Unterlisten.
// Result: [[1, 2], [2, 3], [3, 4], [4, 5]]
List<List<Integer>> slidingWindows = numbers.stream()
.gather(Gatherers. windowSliding(2))
.toList();
mapConcurrent
Parallele Ausführung des Mappings im Gatherer, dabei wird die Ordnung des Streams erhalten.
// Result: true - proves that order was kept, // because each sliding window has a difference of 1.
IntStream.range(0, 10000)
.boxed()
.gather(Gatherers.mapConcurrent(100, x -> x + 1))
.gather(Gatherers.windowSliding(2))
.allMatch(pair -> pair.getLast() - pair.getFirst() == 1);
Scoped Values
Scoped Values JEP 506 sind eine Modernisierung von ThreadLocal. Mit ScopedValue definieren wir Objekte, die nur im Rahmen definierter Threads sichtbar sind. Im folgenden Beispiel startet die main-Methode einen Thread. Im Thread wird nur die Methode printScopedValue ausgeführt. Diese Methode greift per get() auf das Integer-Objekt innerhalb des Scopes zu, welches mit ScopedValue.where für die Ausführung des Threads gesetzt wurde.
private static final ScopedValue<Integer> X = ScopedValue.newInstance();
private static void printScopedValue() {
IO.println("Instead of method-parameter a scoped value is used: " + X.get());
}
void main() {
ScopedValue.where(X, 666).run(() -> printScopedValue());
IO.println("Is scoped value bound in main thread: " + X.isBound()); ... }
Das Beispiel zeigt 2 Vorteile von ScopedValues:
- Werte im Scope des Threads müssen nicht per Methoden-Parameter übergeben werden. Bei einem Wert ist das sicherlich kein Problem, ab 3 Parametern ist es aus Clean Code Sicht problematisch. Mit ScopedValue machen wir beliebig viele Objekte durch mehrfache where-Aufrufe in einem Thread verfügbar.
- Außerhalb des Scopes sind die Werte nicht mehr verfügbar und werden so auch nicht unnötig lang im Speicher gehalten. Deshalb liefert X.isBound() im obigen Beispiel den Wert false. Ein Aufruf von X.get() liefert nur innerhalb des Thread den Wert 666. Außerhalb des Scopes (z.B. in der main-Methode) wirft es eine NoSuchElementException.
Hier ein weiteres Beispiel zur Veranschaulichung der NoSuchElementException. Im 2. Aufruf von thread.run() wird diese Exception geworfen, weil Y nur beim ersten Thread-Aufruf (.run(thread)) einen Wert im Scope hat.
private void scopedValueExample2() {
ScopedValue<String> Y = ScopedValue.newInstance();
Runnable thread = () -> {
try {
IO.println("Print scoped value: " + Y.get());
} catch (NoSuchElementException _) {
IO.println("Scoped value was not bound to this thread.");
}
};
ScopedValue.where(Y, "12345").run(thread);
thread.run();
IO.println("Is scoped value bound in main thread: " + Y.isBound());
}
Fazit
Meine Java 25 Highlights sind Unbenannte Variablen, Gatherers und die Performance-Tunings, weil ich von diesen 3 Features im Berufsalltag am meisten profitieren werde.
Die Vereinfachungen um die main-Methode kommen Java-Neulingen mit Sicherheit zugute. Einsteiger in die Programmierung können sich so besser auf das Wesentliche konzentrieren. Sie lernen zuerst Schleifen, Variablen und if-Statements lernen, bevor sie mit Klassen und statischem Code "kämpfen" müssen.
Das Update unserer Spring Boot Anwendung von Java 21 auf Java 25 verlief problemlos. Wir mussten nur die Java-Version in der Maven-Konfiguration anpassen.
Den kompletten Code zu diesem Artikel gibt es hier:
https://github.com/elmar-brauch/java25
https://github.com/elmar-brauch/java25