Zum Inhalt springen

Java 28 Features (mit Beispielen)

Eine 28 aus gerösteten Kaffeebohnen auf einem hellen Eichenbrett vor einer smaragdgrünen Wand, davor links eine Handvoll Kaffeebohnen, die sich alle unterscheiden, rechts sechs gleiche, in Bronze gegossene Kaffeebohnen auf einem Messingtablett, daneben ein Farn in einem weißen Topf

Java 28 ist noch in Arbeit. Laut Zeitplan soll es am 9. März 2027 erscheinen. Am 3. Dezember 2026 beginnt die sogenannte „Rampdown Phase One“ – ab dann werden keine weiteren JDK Enhancement Proposals (JEPs) mehr aufgenommen. Bis dahin kommen üblicherweise noch weitere JEPs hinzu, und in seltenen Fällen wird ein bereits eingeplanter JEP auch wieder entfernt. Diesen Artikel ergänze ich, sobald sich etwas ändert. Die aktuelle Early-Access-Version von Java 28 kannst du hier herunterladen.

Stand 8. November 2026 sind zehn JEPs für Java 28 eingeplant. Das mit Abstand größte Feature sind Value Objects: Seit 2014 arbeitet das JDK-Team im Rahmen von Project Valhalla an Objekten ohne Identität – mit Java 28 kommt die erste Preview-Version ins JDK. Weil ein Value Object nur durch seine Feldwerte definiert ist, dürfen diese nie in einem anderen als ihrem endgültigen Zustand sichtbar werden – das garantiert in der JVM Strict Field Initialization.

Zwei Features werden nach mehreren Preview-Runden final, und zwar ohne weitere Änderungen: Structured Concurrency und die PEM-API. Neu als Incubator-Feature ist eine JSON-API, mit der du JSON ohne externe Bibliothek lesen und schreiben kannst.

Mehrere JEPs verbessern die Performance: Im AOT-Cache wird jetzt auch kompilierter Maschinencode abgelegt; der ZGC startet und wärmt sich schneller auf und kann die Heap-Größe selbst anpassen; und Shenandoah läuft standardmäßig im Generational Mode. Außerdem wird der macOS/x64-Port als deprecated markiert.

Für alle JEPs verwende ich wie immer die originalen, englischen Bezeichnungen.

Value Objects (Preview) – JEP 401

Jedes Java-Objekt hat eine Identität: Zwei Objekte mit gleichen Feldwerten sind trotzdem zwei verschiedene Objekte, und der ==-Operator vergleicht nicht ihren Inhalt, sondern ihre Identität. Für unveränderliche Daten wie Zahlen, Datumsangaben oder Geldbeträge ist diese Identität überflüssig – sie führt zu Überraschungen und kostet Performance.

Eine Überraschung, die wohl die meisten Java-Anfänger:innen schon einmal erlebt haben:

void main() {
  Integer i = 1996, j = 1996;
  IO.println(i == j);

  LocalDate d1 = LocalDate.of(2027, 3, 9);
  LocalDate d2 = d1.plusYears(1).minusYears(1);
  IO.println(d1 == d2);
}

Beide Vergleiche ergeben false, obwohl die verglichenen Objekte jeweils denselben Wert repräsentieren.

Und die Performance: Die JVM muss jedes Objekt auf dem Heap allokieren und über einen Pointer darauf zugreifen. Ein Array von LocalDate-Objekten ist deshalb ein Array von Pointern auf einzelne, möglicherweise weit verstreut im Speicher liegende Objekte. Das kostet Speicher, und beim Durchlaufen eines solchen Arrays muss jedem Pointer zu seinem Objekt gefolgt werden. Das erfordert für jedes Objekt einen zusätzlichen Speicherzugriff. Und liegen die Objekte verstreut im Speicher, muss die CPU für fast jedes davon eine neue Cache-Line aus dem Hauptspeicher laden.

JDK Enhancement Proposal 401 führt deshalb Value Classes ein – Klassen, deren Instanzen, die Value Objects, keine Identität haben. Eine Value Class deklarierst du mit dem neuen Modifier value. Sie ist implizit final, und auch alle ihre Felder sind implizit final. Besonders gut eignen sich Records:

value record Point(int x, int y) {}

Der ==-Operator vergleicht Value Objects anhand ihrer Feldwerte. Zudem werden mit aktivierten Preview-Features 30 Klassen des JDK zu Value Classes – darunter die Wrapper-Klassen wie Integer und Long, die Optional-Klassen und zahlreiche Klassen aus java.time wie LocalDate, Instant und Duration. Mit der neuen Methode Objects.hasIdentity() kannst du prüfen, ob ein Objekt eine Identität hat:

value record Point(int x, int y) {}

void main() {
  Integer i = 1996, j = 1996;
  IO.println(i == j);

  LocalDate d1 = LocalDate.of(2027, 3, 9);
  LocalDate d2 = d1.plusYears(1).minusYears(1);
  IO.println(d1 == d2);

  Point p = new Point(17, 3);
  IO.println(p == new Point(17, 3));
  IO.println(Objects.hasIdentity(p));
  IO.println(Objects.hasIdentity("Java 28"));
}

Startest du das Programm mit java --enable-preview ValueObjects.java, gibt es Folgendes aus:

true
true
true
false
true

Jetzt sind alle drei Vergleiche true. String ist übrigens keine Value Class – die Klasse hängt in ihrer API und ihrer Implementierung an der Objektidentität.

Was mit Value Objects nicht mehr geht, ist die Synchronisation. Ein synchronized auf eine Variable vom Typ einer Value Class lehnt bereits der Compiler ab. Ist der statische Typ dagegen z. B. Object, wirft die JVM zur Laufzeit eine IdentityException:

void main() {
  Object o = LocalDate.of(2027, 3, 9);
  synchronized (o) {
    IO.println("locked");
  }
}

Die Meldung lautet: Cannot synchronize on an instance of value class java.time.LocalDate.

Da Value Objects keine Identität haben, darf die JVM sie im Speicher frei anordnen. Den Elementen eines LocalDate-Arrays etwa kann sie Jahr, Monat und Tag zusammen mit einem Null-Flag direkt in je ein 64-Bit-Wort schreiben, statt dort Pointer auf einzelne Objekte abzulegen („Flattening“) – das Array verhält sich dann ähnlich wie ein int-Array. Und in Parametern und lokalen Variablen kann sie die Felder eines Value Objects auf dem Stack oder in CPU-Registern statt auf dem Heap halten („Scalarization“). Welche dieser Optimierungen die JVM anwendet, legt der JEP allerdings nicht fest.

Damit ein Value Object nie halb initialisiert sichtbar wird, gelten für die Konstruktoren von Value Classes strengere Regeln: Alle Felder müssen gesetzt sein, bevor der Super-Konstruktor aufgerufen wird. Den impliziten super()-Aufruf setzt der Compiler deshalb ans Ende des Konstruktors, und vorher darf der Konstruktor this nicht verwenden – also z. B. keine Instanzmethoden aufrufen. Das baut auf den Flexible Constructor Bodies aus Java 25 auf.

Mit aktivierten Preview-Features gelten diese Regeln übrigens für alle Records – auch für solche ohne value. Das ist eine inkompatible Änderung: Ein Record-Konstruktor, der z. B. this an eine Methode übergibt, kompiliert mit --enable-preview nicht mehr. Laut JEP kommt das in der Praxis selten vor, und meist ist ein solcher Zugriff ohnehin ein Bug, weil die Felder des Records zu diesem Zeitpunkt noch nicht gesetzt sind.

Strict Field Initialization in the JVM (Preview) – JEP 539

Felder, die nicht explizit initialisiert werden, haben in Java einen Default-Wert: 0, false oder null. Und auch ein finales Feld kann seinen Default-Wert zeigen, solange seine Klasse oder sein Objekt noch initialisiert wird. Das folgende Beispiel aus dem JEP (leicht gekürzt) zeigt, wie das bei einer zyklischen Abhängigkeit zwischen zwei Klassen passieren kann:

class App {

  public static final long appID = Log.currentPID();

  public static void main() {
    IO.println("App[" + appID + "] has started");
    Log.log("Completed 'main'");
  }
}

class Log {

  private static final String prefix = "App[" + App.appID + "]: ";

  public static void log(String msg) {
    IO.println(prefix + msg);
  }

  public static long currentPID() {
    return ProcessHandle.current().pid();
  }
}

Das Programm gibt Folgendes aus (die Prozess-ID variiert natürlich):

App[66877] has started
App[0]: Completed 'main'

Der Aufruf von Log.currentPID() bei der Initialisierung von App.appID löst die Initialisierung der Klasse Log aus. Diese liest App.appID, das zu diesem Zeitpunkt noch seinen Default-Wert 0 hat, und baut ihn in prefix ein. Erst danach erhält appID die tatsächliche Prozess-ID.

Ein zweites Beispiel ist die Vererbung: Ruft ein Super-Konstruktor eine Methode auf, die in der Subklasse überschrieben ist, sieht diese ein finales Feld der Subklasse noch in seinem uninitialisierten Zustand null. Wie das aussieht, habe ich im Artikel über Java 25 unter Weniger Überraschungen bei Vererbung gezeigt.

JDK Enhancement Proposal 539 führt in der JVM strikt initialisierte Felder ein. Ein solches Feld hat keinen Default-Wert: Die JVM stellt sicher, dass es geschrieben wird, bevor es gelesen werden kann, und dass ein finales Feld nach dem ersten Lesen nicht mehr verändert wird. Bei statischen Feldern wirft die JVM bei einem Verstoß eine Exception, bei Instanzfeldern schlägt bereits die Bytecode-Verifikation fehl. Markiert werden solche Felder im Class-File durch das neue Flag ACC_STRICT_INIT.

Wichtig: JEP 539 ist ein reines JVM-Feature. Es gibt kein neues Schlüsselwort, und beide Beispiele verhalten sich in Java 28 unverändert. Welche Felder strikt initialisiert werden, entscheidet der Compiler – javac tut das für alle Felder von Value Classes und, mit aktivierten Preview-Features, auch für die Felder aller Records. Value Objects bauen also auf JEP 539 auf: Da ein Value Object nur durch seine Feldwerte definiert ist, darf niemand ein Feld je mit einem anderen Wert sehen als seinem endgültigen.

Außerdem kann der JIT-Compiler strikt initialisierte finale Felder als unveränderlich behandeln und einen einmal gelesenen Wert wiederverwenden.

Simple JSON API (Incubator) – JEP 540

Wer in Java bisher JSON lesen oder schreiben wollte, brauchte eine externe Bibliothek wie Jackson oder Gson. JDK Enhancement Proposal 540 bringt eine eigene kleine JSON-API ins JDK, zunächst als Incubator-Modul jdk.incubator.json.

Die API soll die etablierten Bibliotheken nicht ersetzen: Data Binding (also das Umwandeln von JSON in Java-Objekte und zurück), Streaming und Syntaxerweiterungen wie Kommentare sind ausdrücklich keine Ziele. Ziel ist es, einfache Aufgaben – etwa einzelne Werte aus einer REST-Antwort oder einer Konfigurationsdatei auszulesen – mit wenig Code und ohne Dependencies zu erledigen.

Im Zentrum steht das sealed Interface JsonValue mit den sechs Subtypen JsonString, JsonNumber, JsonBoolean, JsonNull, JsonObject und JsonArray. Json.parse() wandelt einen String in einen Baum aus JsonValue-Objekten um und liefert dessen Wurzel als JsonValue. Die Methoden get(String) (für Objekte) und get(int) (für Arrays) sind bereits in JsonValue deklariert. Du kannst damit also direkt durch Objekte und Arrays navigieren, ohne einen Wert vorher nach JsonObject oder JsonArray casten zu müssen. Methoden wie asString(), asInt() oder asList() wandeln die Werte in Java-Typen um. Für optionale Felder gibt es tryGet(), das ein Optional liefert:

import jdk.incubator.json.*;

void main() {
  String doc = """
      {
        "version": 28,
        "ga": "2027-03-09",
        "jeps": [
          { "number": 401, "title": "Value Objects", "preview": true },
          { "number": 543, "title": "Structured Concurrency" }
        ]
      }
      """;

  JsonValue root = Json.parse(doc);
  IO.println("Java " + root.get("version").asInt()
      + ", GA: " + root.get("ga").asString());

  for (JsonValue jep : root.get("jeps").asList()) {
    boolean preview = jep.tryGet("preview").map(JsonValue::asBoolean).orElse(false);
    IO.println("JEP " + jep.get("number").asInt() + ": " + jep.get("title").asString()
        + (preview ? " (Preview)" : ""));
  }

  IO.println(JsonObject.of(Map.of("version", JsonNumber.of(28))));
}

Die letzte Zeile zeigt den umgekehrten Weg: Sie erzeugt ein JSON-Objekt und gibt es als String aus. Die Werte musst du dabei selbst in JsonValue-Objekte verpacken, denn JsonObject.of() erwartet eine Map<String, ? extends JsonValue>. Eine einfache 28 statt JsonNumber.of(28) würde also ebenso wenig kompilieren wie ein String, eine Liste, ein Array oder ein beliebiges anderes Objekt. Eine Liste z. B. musst du mit JsonArray.of() verpacken – und deren Elemente ebenfalls einzeln.

Da es sich um ein Incubator-Modul handelt, musst du es beim Start explizit hinzufügen: java --add-modules jdk.incubator.json JsonDemo.java. Die Ausgabe:

WARNING: Using incubator modules: jdk.incubator.json
Java 28, GA: 2027-03-09
JEP 401: Value Objects (Preview)
JEP 543: Structured Concurrency
{"version":28}

Fehlt ein Wert oder hat er einen anderen Typ als erwartet, wirft die API eine JsonValueException, deren Meldung den Pfad zur betroffenen Stelle im Dokument enthält. Rufst du z. B. root.get("jeps").get(1).get("preview") auf, lautet die Meldung JsonObject member "preview" does not exist, gefolgt vom Pfad {jeps[1 und der Position im Dokument (Zeile 5, Position 4). Den Pfad notiert die API in einer eigenen Kurzform: { steht für ein Objekt, gefolgt vom Namen des Members, [ für ein Array, gefolgt vom Index. Geschlossen werden die Klammern nicht – {jeps[1 bedeutet also: Member jeps des Wurzelobjekts, darin Element 1.

Structured Concurrency – JEP 543

Nach zwei Incubator-Runden in Java 19 und Java 20 sowie sieben Preview-Runden von Java 21 bis Java 27 wird Structured Concurrency in Java 28 durch JDK Enhancement Proposal 543 final – ohne weitere Änderungen gegenüber Java 27.

Mit Structured Concurrency fasst du nebenläufige Subtasks zu einer Einheit zusammen: Die Subtasks werden in einem StructuredTaskScope gestartet und laufen an einer Stelle im Code wieder zusammen. Schlägt einer von ihnen fehl, werden die übrigen abgebrochen, und join() wirft eine ExecutionException mit der ursprünglichen Exception als Cause:

ProductPage loadProductPage(long productId)
    throws InterruptedException, ExecutionException {
  try (var scope = StructuredTaskScope.open()) {
    var detailsTask = scope.fork(() -> catalogService.getDetails(productId));
    var priceTask   = scope.fork(() -> pricingService.getPrice(productId));
    var stockTask   = scope.fork(() -> inventoryService.getStock(productId));
    scope.join();
    return ProductPage.assemble(
        detailsTask.get(), priceTask.get(), stockTask.get());
  }
}

Ab Java 28 brauchst du dafür kein --enable-preview mehr. Wie sich die API in den letzten Preview-Runden verändert hat, kannst du in den Artikeln zu Java 25, Java 26 und Java 27 nachlesen. Eine ausführliche Einführung mit allen Joinern, Timeouts und eigenen Policies findest du im Hauptartikel Structured Concurrency mit StructuredTaskScope.

PEM Encodings of Cryptographic Objects – JEP 542

Kryptografische Objekte wie Schlüssel und Zertifikate werden häufig im Textformat PEM (Privacy-Enhanced Mail) ausgetauscht. In Java 25 wurde dafür eine API eingeführt, die das Lesen und Schreiben solcher Objekte stark vereinfacht. Nach drei Preview-Runden in Java 25, Java 26 und Java 27 wird sie in Java 28 durch JDK Enhancement Proposal 542 final – ohne weitere Änderungen.

Das folgende Beispiel erzeugt ein Schlüsselpaar, schreibt den privaten Schlüssel verschlüsselt im PEM-Format und liest ihn wieder ein:

import java.security.*;

void main() throws Exception {
  KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair();
  char[] passphrase = "secret".toCharArray();

  String pem = PEMEncoder.of()
      .withEncryption(passphrase)
      .encodeToString(keyPair.getPrivate());
  IO.print(pem);

  PrivateKey privateKey = PEMDecoder.of()
      .withDecryption(passphrase)
      .decode(pem, PrivateKey.class);
  IO.println(privateKey.equals(keyPair.getPrivate()));
}

Das Programm gibt den privaten Schlüssel im PEM-Format aus und bestätigt, dass der eingelesene Schlüssel dem ursprünglichen entspricht (der Schlüssel ist natürlich bei jedem Lauf ein anderer):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHKMGYGCSqGSIb3DQEFDTBZMDgGCSqGSIb3DQEFDDArBBQWZWQexym0y0AhZ5x/
2dLqLIZqhgICEAACARAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEFFSbxsw
2NfBZbCyywgGaP8EYBI91wkxuzTX+qeLVesmGT79us9NBRh5DTR9pf13+u6yVoM3
S96MrWi7ITJLmtda0i1AaHpQbuFnD8lxgEJiWOE0kSwToGurxLv0q6AcOjihOjED
2GvadzzbzGEIvH/e+w==
-----END ENCRYPTED PRIVATE KEY-----
true

Wie umständlich das ohne die neue API war, zeigt der Abschnitt im Java-27-Artikel.

Performance-Verbesserungen

Ahead-of-Time Code Compilation – JEP 544

Seit Java 24 kann die JVM in einem Trainingslauf einen AOT-Cache erzeugen, aus dem sie Klassen beim nächsten Start in geladener und gelinkter Form übernimmt. In Java 25 kamen Methodenprofile hinzu, und seit Java 26 funktioniert der Cache mit jedem Garbage Collector.

JDK Enhancement Proposal 544 legt in Java 28 zusätzlich fertig kompilierten Maschinencode der häufig aufgerufenen Methoden im AOT-Cache ab. Dafür musst du nichts umstellen – derselbe Trainingslauf, der seit Java 25 den Cache erzeugt, kompiliert jetzt auch den Code:

java -XX:AOTCacheOutput=AotCompile.aot -cp AotCompile.jar AotCompile 20

An einem Beispielprogramm, das in jeder Runde 50 generierte Quelldateien mit dem javac-Compiler im Speicher übersetzt, halbiert der vorkompilierte Code die Ausführungszeit der ersten Runde: 252 statt 527 Millisekunden. Der JIT-Compiler läuft dabei weiter und ersetzt den vorkompilierten Code nach und nach durch neu kompilierten Code. Wie das abläuft, wie lang der Trainingslauf sein sollte und unter welchen Bedingungen die JVM den Maschinencode nicht lädt, erfährst du im Artikel Ahead-of-Time Code Compilation.

Faster Startup and Warmup with ZGC – JEP 545

Beim Z Garbage Collector (ZGC) hat die initiale Heap-Größe (-Xms) großen Einfluss auf Start und Aufwärmphase einer Anwendung. Physischen Speicher vom Betriebssystem anzufordern und für den Heap vorzubereiten, ist teuer: Mit einem kleinen initialen Heap startet die JVM schnell, muss den Heap aber während der Aufwärmphase Stück für Stück vergrößern. Mit einem großen initialen Heap ist es umgekehrt. Laut JEP startet die JVM mit dem ZGC und einem 2-MB-Heap in etwa einer Zehntelsekunde, mit einem 32-GB-Heap braucht sie über zehn Sekunden.

JDK Enhancement Proposal 545 beseitigt diesen Zielkonflikt:

  • Der ZGC startet mit einem Heap von nur 2 MB.
  • Er vergrößert den Heap aggressiv, in immer größeren Schritten – und zwar in eigenen Worker-Threads statt in den Threads der Anwendung. Dafür verwendet er höchstens 25 % der verfügbaren CPU-Threads.
  • Den neu angeforderten Speicher beschreibt er schon beim Vergrößern einmal Seite für Seite (Pre-Touching). So trägt das Betriebssystem die Seiten sofort in die Page Table ein, und die Anwendung kann den Speicher gleich beim ersten Zugriff ohne Verzögerung nutzen.
  • Unter Linux ab Version 6.1 wandelt er kleine Speicherseiten (4 KB) nach und nach in große (2 MB) um – ohne dass dafür der Kernel umkonfiguriert werden muss.

Im Beispiel des JEPs, der Spring-PetClinic-Anwendung auf einem Rechner mit 32 GB RAM, sinkt die Startzeit gegenüber einem großen Heap mit Pre-Touching beim Start um 38 %, und die Aufwärmphase verkürzt sich gegenüber einem kleinen Heap von 80 auf 10 Sekunden.

Diese Verbesserungen greifen, wenn du zusätzlich zum ZGC das im nächsten Abschnitt beschriebene Adaptive Heap Sizing aktivierst:

java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing ...

Adaptive Heap Sizing for ZGC – JEP 546

Die maximale Heap-Größe gut zu wählen, ist schwierig: Standardmäßig setzt die JVM sie auf ein Viertel des physischen Speichers. Ist der Heap zu klein, muss der Garbage Collector häufiger aufräumen und verbraucht mehr CPU-Zeit. Ist er zu groß, kann dem Betriebssystem der Speicher ausgehen, sodass es Speicherseiten auf die Festplatte auslagert oder im schlimmsten Fall Prozesse beendet.

JDK Enhancement Proposal 546 führt für den ZGC eine optionale Strategie ein, die die Heap-Größe automatisch anpasst. Ist sie aktiviert, gilt Folgendes:

  • Der Heap startet mit 2 MB.
  • Die maximale Heap-Größe liegt standardmäßig bei 100 % des physischen Speichers – auf meinem Mac mit 64 GB RAM zeigt -XX:+PrintFlagsFinal eine MaxHeapSize von 64 GB. Dass der Speicher dabei nicht knapp wird, regelt der letzte Punkt.
  • Der ZGC passt die Heap-Größe laufend so an, dass Speicherverbrauch und CPU-Verbrauch des Garbage Collectors in einem guten Verhältnis stehen.
  • Er beobachtet, wie viel Speicher und CPU-Zeit die JVM im Vergleich zum Gesamtsystem verbraucht, und gibt Speicher ab, wenn andere Prozesse ihn brauchen.

Aktiviert wird Adaptive Heap Sizing mit -XX:+ZAdaptiveHeapSizing; standardmäßig ist es ausgeschaltet. Mit -XX:ZGCIntensity kannst du das Verhältnis zwischen dem Speicherverbrauch und dem CPU-Verbrauch des Garbage Collectors nachjustieren: Der Standardwert ist 5, niedrigere Werte lassen den ZGC seltener aufräumen – er braucht dann weniger CPU-Zeit, belegt aber mehr Speicher –, höhere Werte bewirken das Gegenteil. Setzt du nur -XX:ZGCIntensity, schaltet die JVM Adaptive Heap Sizing automatisch mit ein.

Mit Adaptive Heap Sizing musst du -Xmx und -XX:SoftMaxHeapSize laut JEP nicht mehr setzen – vorhandene Einstellungen funktionieren aber weiterhin.

Shenandoah GC: Generational Mode by Default – JEP 535

Der Generational Mode des Shenandoah-Garbage-Collectors wurde in Java 24 als experimentelles Feature eingeführt und in Java 25 zu einem produktiven Feature erklärt.

In Java 28 wird er durch JDK Enhancement Proposal 535 zum Standardmodus: Der Default-Wert der Option ShenandoahGCMode ändert sich von satb auf generational. Den nicht-generationellen Modus kannst du weiterhin mit -XX:ShenandoahGCMode=satb aktivieren; die JVM gibt dann aber eine Warnung aus, dass der nicht-generationelle Modus als deprecated markiert ist und in einer künftigen Version entfernt werden soll.

Sonstige Änderungen in Java 28

Deprecate the macOS/x64 Port for Removal – JEP 541

Apple hat seine Rechner von 2020 bis 2023 auf die AArch64-Architektur umgestellt („Apple Silicon“) und stellt die Unterstützung für x64 schrittweise ein. Oracle pflegt den macOS/x64-Port deshalb seit JDK 27 nicht mehr.

Durch JDK Enhancement Proposal 541 wird der Port in Java 28 als deprecated markiert und soll in einer künftigen Version entfernt werden. Wer das JDK für macOS/x64 selbst baut, muss dafür ab sofort die Option --enable-deprecated-ports angeben – eine Garantie, dass der Port noch baut oder funktioniert, gibt es nicht mehr.

Fazit

Stand heute bringt Java 28 die folgenden Neuerungen:

Bis zur Rampdown Phase One am 3. Dezember 2026 können noch weitere JEPs hinzukommen – ich ergänze sie hier, sobald sie für Java 28 eingeplant sind.

Was soll ich als Nächstes vertiefen? Das erfahre ich am besten durch dein Feedback: Eine Bewertung auf meinem ProvenExpert-Profil zeigt mir, welche Themen dir wichtig sind – und motiviert mich, weitere Artikel zu schreiben.

👉 Bewertung abgeben

Dieses Thema im eigenen Code?

Du hast den Artikel gelesen – im Training arbeitet ihr damit. 2 Tage lang gehen wir die Themen an euren eigenen Projekten durch, statt an konstruierten Beispielen.

Praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Java 25 SchulungAlle Trainings ansehen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche