actu-image
Software engineering PL - 23 paź 2025

Java 25: najważniejsze wnioski i nowości

Image vignette
Photo Achraf Hasbi

Autor: Achraf Hasbi ,
Practice Leader, coach i ekspert Java w MARGO.

Najważniejsze nowości Java 25, które warto znać

Java 25: wydajność, bezpieczeństwo i produktywność

Premiera Javy 25 to ważny kamień milowy dla całego ekosystemu. W ramach regularnego cyklu wydań Oracle Java 25 wprowadza szereg funkcji, które zwiększają ekspresyjność języka i optymalizują wydajność JVM.

Ta wersja obejmuje 18 JEP-ów: 12 finalnych, 4 w wersji preview, 1 eksperymentalny i 1 inkubacyjny.

W tym artykule omawiamy nowości Javy 25, skupiając się na finalnych JEP-ach i ich wpływie na programowanie w Javie.

Pobierz OpenJDK 25 tutaj: OpenJDK 25

JVM: nowości w Javie 25

JEP 503: Remove the 32-bit x86 Port

JEP 503 zakłada usunięcie z JDK portu 32-bit x86. Po jego wycofaniu w JEP 501 decyzja ta ma na celu usunięcie powiązanego kodu źródłowego i wsparcia dla budowania.

Głównym powodem usunięcia jest rosnący koszt utrzymania portu 32-bit x86. Dostosowanie go do nowoczesnych funkcji, takich jak Project Loom, Foreign Function & Memory (FFM) API czy Vector API, okazało się kosztowne. Trwałe usunięcie tego portu pozwala deweloperom OpenJDK szybciej dostarczać nowe funkcje i ulepszenia.

JEP 514: Early Command-Line Ergonomics

JEP 514 upraszcza tworzenie pamięci podręcznej Ahead-Of-Time (AOT cache), która przyspiesza uruchamianie aplikacji Java, skracając polecenia potrzebne w typowych przypadkach użycia.

Przed JEP 514 utworzenie AOT cache wymagało dwóch kroków:

  • 1. Trening: uruchomienie aplikacji w celu zarejestrowania jej zachowania.
  • 2. Tworzenie: wygenerowanie AOT cache na podstawie zarejestrowanych danych.

Proces był uciążliwy i często pozostawiał tymczasowe pliki konfiguracyjne. JEP 514 rozwiązuje te problemy, wprowadzając proces jednoetapowy, który jest wydajny i wygodny w użyciu, a przy tym otwiera drogę do szerszego stosowania optymalizacji AOT w miarę postępów Project Leyden.

JEP 514 wprowadza nową opcję wiersza poleceń -XX:AOTCacheOutput w programie uruchomieniowym java (launcherze). Użyta samodzielnie pozwala mu wykonać zarówno trening, jak i utworzenie pamięci podręcznej jednym poleceniem. Dodatkowo nowa zmienna środowiskowa JDK_AOT_VM_OPTIONS pozwala przekazać opcje wiersza poleceń przeznaczone wyłącznie do tworzenia pamięci podręcznej, bez wpływu na przebieg treningu.

Przed JEP 514

# Step 1: Training run
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App

# Step 2: Create AOT cache
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot app.jar com.example.App

# Production run
java -XX:AOTCache=app.aot -cp app.jar com.example.App

Po JEP 514

# Single command to train and create AOT cache
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App

# Production run
java -XX:AOTCache=app.aot -cp app.jar com.example.App

JEP 515: Early Method Profiling

JEP 515 umożliwia zapisanie profili wykonania metod z poprzedniego uruchomienia (treningu) i ich ponowne użycie przy starcie aplikacji. Celem jest umożliwienie kompilatorowi JIT w HotSpot JVM generowania kodu natywnego od razu po starcie, zamiast czekania na zebranie profili podczas pierwszego wykonania.

Funkcja rozszerza istniejący AOT cache (wprowadzony w JEP 483): przechowuje on teraz nie tylko klasy ładowane lub linkowane przy starcie, ale też profile wykonania metod zebrane podczas treningu. Nie wyłącza ani nie zastępuje profilowania w czasie działania: nawet z zapisanymi profilami JVM nadal zbiera dane profilowania na produkcji, bo rzeczywiste zachowanie może różnić się od środowiska treningowego.

JEP 518: Cooperative JFR Sampling

JEP 518 poprawia stabilność JDK Flight Recorder (JFR) podczas asynchronicznego próbkowania stosów wątków Javy. Zamiast analizować ramki stosu w dowolnych momentach, co mogło prowadzić do awarii lub błędnych śladów stosu, JFR przechodzi i analizuje stosy wyłącznie w safepointach.

Aby ograniczyć problemy wynikające z analizowania stosów tylko w safepointach (tzw. safepoint bias), JFR stosuje próbkowanie kooperacyjne: szybko rejestruje „żądania próbki” (na przykład tylko licznik programu i wskaźnik stosu), a pełne przechwycenie stosu odkłada do chwili, gdy wątek docelowy osiągnie następny safepoint.

Zgodnie z JEP 518, gdy przychodzi czas na pobranie próbki czasu wykonania JFR:

  • 1. Wątek próbkujący wstrzymuje wątek docelowy i zapisuje w żądaniu tylko lekkie informacje (licznik programu + wskaźnik stosu). Nie próbuje od razu analizować ani przechodzić stosu (chyba że wątek jest już w safepoincie).
  • 2. Umieszcza to „żądanie próbki” w kolejce (lokalnej dla wątku), a następnie pozwala wątkowi docelowemu kontynuować wykonanie aż do następnego safepointu.
  • 3. W safepoincie wątek docelowy sprawdza oczekujące żądania próbek; dla każdego z nich odtwarza bezpieczny ślad stosu i emituje zdarzenie próbkowania.

JEP 519: Compact Object Headers

JEP 519 zmienia status Compact Object Headers z eksperymentalnego na produkcyjny (stabilny) w JDK 25. Oznacza to, że ich włączenie nie wymaga już flagi „experimental”.

Przed JEP 519

java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -jar myapp.jar

Po JEP 519

java -XX:+UseCompactObjectHeaders -jar myapp.jar

Kompaktowe nagłówki obiektów pojawiły się w JDK 24 w ramach JEP 450 jako funkcja eksperymentalna, której celem było zmniejszenie rozmiaru nagłówków obiektów. Od tego czasu wykazały dużą stabilność i wydajność: na produkcji (w usługach Oracle i Amazon) i w benchmarkach przyniosły oszczędność pamięci, krótszy czas CPU i mniej cykli GC.

JEP 520: JFR Method Timing & Tracing

JEP 520 dodaje do JDK Flight Recorder (JFR) pomiar czasu i śledzenie metod dzięki instrumentacji kodu bajtowego. Wprowadza dwa nowe zdarzenia JFR: jdk.MethodTiming (do rejestrowania liczby i czasu wykonań) oraz jdk.MethodTrace (do śledzenia pojedynczych wywołań metod wraz ze śladami stosu). Metody do pomiaru lub śledzenia można wybrać za pomocą filtrów, konfiguracji, jcmd, JMX albo argumentów startowych JVM.

JEP 521: Generational Shenandoah

JEP 521 nadaje trybowi generacyjnemu garbage collectora Shenandoah status funkcji produkcyjnej w JDK 25. Wcześniej tryb ten był eksperymentalny (wprowadzony w JDK 24 przez JEP 404).

W JDK 24, aby korzystać z trybu generacyjnego Shenandoah, trzeba było przekazać określone opcje wiersza poleceń VM, w tym:

-XX:+UseShenandoahGC
-XX:+UnlockExperimentalVMOptions
-XX:ShenandoahGCMode=generational

W JDK 25, ponieważ jest to już funkcja produkcyjna, opcja -XX:+UnlockExperimentalVMOptions nie jest już wymagana. Tryb generacyjny włączysz po prostu poleceniem:

-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=generational

JEP 511: Module Import Declarations

JEP 511 wprowadza do języka Java deklaracje importu modułów. Deklaracja importu modułu ma postać import module M; i pozwala zaimportować wszystkie publiczne klasy i interfejsy najwyższego poziomu ze wszystkich pakietów eksportowanych przez moduł M, w tym, w stosownych przypadkach, z modułów, od których M zależy (przechodnio). Funkcja ma uprościć korzystanie z modułowych API, zmniejszając liczbę standardowych instrukcji importu. W JDK 25 jest finalna, po fazach preview w poprzednich wersjach.

Przed JEP 511

// Without module import
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.nio.file.Path;
import java.io.IOException;

public class MyApp {
  public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
    Path p = Path.of("file.txt");
    // ... etc
  }
}

Potrzebujesz osobnych instrukcji importu dla każdego pakietu, pakietu na żądanie lub klasy.

Po JEP 511

// With module import
import module java.base;
import module java.util;
import module java.io;

public class MyApp {
  public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
    Path p = Path.of("file.txt");
    // ... etc
  }
}

Jeszcze prościej

import module java.base;
import module java.io;

public class MyApp {
  public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
    Path p = Path.of("file.txt");
    // etc
  }
}

Ponieważ java.base importuje java.util i java.nio.file, import modułu java.base pozwala włączyć wiele z tych elementów bez importowania każdego pakietu czy klasy z osobna. (Uwaga: w zależności od tego, jak moduły eksportują pakiety i jakie mają zależności przechodnie, niektóre importy modułów mogą już zawierać potrzebne klasy, co dodatkowo porządkuje importy).

JEP 512: Compact Source Files and Instance Main Methods

JEP 512 finalizuje zestaw funkcji, które ułatwiają pisanie i czytanie programów w Javie, zwłaszcza krótkich programów dla początkujących, bez tworzenia nowego dialektu. Pozwala na kompaktowe pliki źródłowe: w małych programach nie trzeba deklarować klasy, modułu ani pakietu, metody i pola można pisać bezpośrednio.

Wprowadza też nową klasę java.lang.IO do podstawowej obsługi konsoli, dzięki czemu początkujący mogą pisać IO.println(...) zamiast System.out.println(...).

Przed JEP 512

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

Po JEP 512

void main() {
  IO.println("Hello, World!");
}

JEP 513: Flexible Constructor Bodies

JEP 513 pozwala umieszczać w ciele konstruktora instrukcje przed jawnym wywołaniem innego konstruktora (tj. super(...) lub this(...)). Instrukcje te nie mogą odwoływać się do tworzonego obiektu (przez this), z wyjątkiem inicjalizacji jego własnych, jeszcze niezainicjalizowanych pól.

Przed JEP 513

class Person {
  int age;
  Person(int age) {
    if (age < 0)
      throw new IllegalArgumentException("Age must be >= 0");
    this.age = age;
  }
}

class Employee extends Person {
  String officeID;
  Employee(int age, String officeID) {
    // Must call super first (implicitly or explicitly)
    super(age);  
    if (age < 18 || age > 67)
      throw new IllegalArgumentException("Employee age must be between 18 and 67");
    this.officeID = officeID;
  }
}

Po JEP 513

class Person {
  int age;
  Person(int age) {
    if (age < 0)
      throw new IllegalArgumentException("Age must be >= 0");
    this.age = age;
  }
}

class Employee extends Person {
  String officeID;
  Employee(int age, String officeID) {
    // Prologue: early validation
    if (age < 18 || age > 67)
      throw new IllegalArgumentException("Employee age must be between 18 and 67");
    // Prologue: initialize subclass field before super
    this.officeID = officeID;
    super(age);  // Now allowed after some checks and field assignment
  }
}

API

JEP 506: Scoped Values

JEP 506 wprowadza Scoped Values, które pozwalają współdzielić niezmienne dane w obrębie wątku, między metodą a jej wywołującymi oraz z wątkami potomnymi. Scoped Values są łatwiejsze do zrozumienia i lżejsze pod względem pamięci i czasu wykonania niż ThreadLocal, zwłaszcza w połączeniu z Virtual Threads (JEP 444) i Structured Concurrency (JEP 505).

Przed JEP 506 (z użyciem ThreadLocal)

public class Framework {
  private static final ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();

  public void serve(Request request, Response response) {
    FrameworkContext ctx = createContext(request);
    CONTEXT.set(ctx);
    try {
      Application.handle(request, response);
    } finally {
      CONTEXT.remove();  // need to clean up to avoid leaks
    }
  }

  public PersistedObject readKey(String key) {
    FrameworkContext ctx = CONTEXT.get();
    if (ctx == null) {
      throw new IllegalStateException("No context");
    }
    Database db = getDatabase(ctx);
    return db.readKey(key);
  }
}

Po JEP 506 (z użyciem Scoped Values)

import java.lang.ScopedValue;
import static java.lang.ScopedValue.where;

public class Framework {
  private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();

  public void serve(Request request, Response response) {
    FrameworkContext ctx = createContext(request);
    where(CONTEXT, ctx).run(() -> {
      Application.handle(request, response);
    });
  }

  public PersistedObject readKey(String key) {
    FrameworkContext ctx = CONTEXT.get();  // safe to call here if inside a run
    if (ctx == null) {
      throw new IllegalStateException("No context");
    }
    Database db = getDatabase(ctx);
    return db.readKey(key);
  }
}

JEP 510: Key Derivation Function API

JEP 510 finalizuje Key Derivation Function (KDF) API w JDK 25. Wprowadzone wcześniej w JEP 478 i udostępnione jako preview w JDK 24, jest teraz częścią standardowych API JDK 25. Ten JEP wprowadza nową klasę javax.crypto.KDF dla algorytmów wyprowadzania kluczy, takich jak HKDF (HMAC-Extract-and-Expand) i inne. Udostępnia metody wyprowadzania obiektów SecretKey (deriveKey) i surowych danych (deriveData), sparametryzowane specyfikacjami algorytmów. Zawiera też klasy specyfikacji parametrów dla HKDF (np. extract, expand, extract-then-expand).

Funkcje wyprowadzania kluczy są powszechnie stosowane we współczesnej kryptografii: do generowania wielu kluczy ze wspólnego sekretu, w protokołach takich jak hybrydowa wymiana kluczy, TLS 1.3, HPKE itp. Standardowe, dobrze zaprojektowane API Javy dla KDF jest kluczowe dla poprawności, interoperacyjności i łatwości utrzymania.

Przed JEP 510

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.SecretKey;
import java.security.SecureRandom;

public class HKDFExampleBefore {
  public static SecretKey deriveAesKey(byte[] ikm, byte[] salt, byte[] info) throws Exception {
    // Extract step
    Mac hmac = Mac.getInstance("HmacSHA256");
    SecretKeySpec saltKey = new SecretKeySpec(salt, "HmacSHA256");
    hmac.init(saltKey);
    byte[] prk = hmac.doFinal(ikm);

    // Expand step
    byte[] okm = new byte[32]; // 256 bits
    byte[] t = new byte[0];
    int iterations = (int) Math.ceil((double)okm.length / hmac.getMacLength());
    byte[] output = new byte[0];
    for (int i = 1; i <= iterations; i++) {
      hmac.init(new SecretKeySpec(prk, "HmacSHA256"));
      hmac.update(t);
      hmac.update(info);
      hmac.update((byte) i);
      t = hmac.doFinal();
      output = concat(output, t);
    }
    byte[] aesKeyBytes = Arrays.copyOf(output, 32);
    return new SecretKeySpec(aesKeyBytes, "AES");
  }
}

Po JEP 510

import javax.crypto.KDF;
import javax.crypto.SecretKey;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.HKDFParameterSpec;

public class HKDFExampleAfter {
  public static SecretKey deriveAesKey(byte[] ikm, byte[] salt, byte[] info) throws Exception {
    // Instantiate the KDF with algorithm name
    KDF hkdf = KDF.getInstance("HKDF-SHA256");

    // Build parameter spec: do extract then expand
    AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract()
      .addIKM(ikm)
      .addSalt(salt)
      .thenExpand(info, 32);  // derive 32 bytes

    // Derive AES key directly
    SecretKey aesKey = hkdf.deriveKey("AES", params);
    return aesKey;
  }
}

JEP-y w wersji preview, eksperymentalne i inkubacyjne w Javie 25

Ten artykuł skupia się na finalnych JEP-ach Javy 25, czyli funkcjach, których możesz bezpiecznie używać na produkcji już dziś, ale wydanie obejmuje też kilka JEP-ów w wersji preview, inkubacyjnych i eksperymentalnych. To funkcje, które wciąż się zmieniają: wersje preview są prawie gotowe, ale mogą przejść drobne poprawki; inkubatory udostępniają API, by zebrać wczesne opinie; funkcje eksperymentalne mają charakter badawczy i mogą zostać znacząco zmienione lub usunięte. Oto przegląd tego, co nadchodzi:

JEP 470: PEM Encodings of Cryptographic Objects (Preview)

Dodaje API do kodowania i dekodowania obiektów kryptograficznych (kluczy publicznych i prywatnych, certyfikatów, list CRL) w powszechnie używanym formacie PEM.

JEP 502: Stable Values (Preview)

Stable values to obiekty zawierające niezmienne dane, ale w przeciwieństwie do pól final ich inicjalizację można odroczyć (inicjalizacja „co najwyżej raz”), a następnie JVM traktuje je niemal jak stałe.

JEP 505: Structured Concurrency (Fifth Preview)

Udostępnia API (przede wszystkim StructuredTaskScope), które pozwala traktować grupę zadań (wątków, w tym wirtualnych) jako jedną ustrukturyzowaną całość: uruchamiać je razem, czekać na nie, czysto obsługiwać błędy i anulowania oraz zarządzać ich cyklem życia w blokach.

JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)

Rozszerza dopasowywanie wzorców, instanceof i switch tak, aby typy proste (np. int, long, float) mogły być używane w kontekstach wzorców, a nie tylko typy referencyjne.

JEP 508: Vector API (10th Incubator)

Udostępnia inkubacyjne API do wyrażania obliczeń wektorowych (w stylu SIMD), dzięki któremu programy w Javie mogą jawnie zapisywać operacje wektorowe mapowane w czasie działania na optymalne instrukcje wektorowe procesora. API nie jest jeszcze finalne: przechodzi obecnie dziesiątą fazę inkubacji.

JEP 509: JFR CPU Time Profiling (Experimental)

Dodaje do Java Flight Recorder profilowanie czasu CPU (na Linuksie, eksperymentalnie). Zamiast próbkowania według czasu zegarowego lub upływającego otrzymujesz próbki oparte na rzeczywistym zużyciu czasu procesora, również przez kod natywny. Nowy typ zdarzenia: jdk.CPUTimeSample.

Podsumowanie

Java 25 potwierdza stabilny rozwój platformy, łącząc dojrzałe funkcje gotowe do produkcji z innowacjami nastawionymi na przyszłość. Finalne JEP-y przynoszą wymierną poprawę wydajności, bezpieczeństwa i produktywności programistów, a funkcje preview, inkubacyjne i eksperymentalne pozwalają zajrzeć w przyszłość języka i JVM. Razem pokazują wyraźne dążenie do tego, by Java pozostała nowoczesna, ekspresyjna i wydajna. Jak zawsze, wdrażanie stabilnych funkcji już dziś i obserwowanie tych, które nadchodzą, pozwoli Ci korzystać z obu światów: niezawodności i innowacji.

Co nowego wnosi Java 25 w porównaniu z Javą 21?

Java 25 wprowadza kilka finalnych funkcji, takich jak uproszczony import modułów (JEP 511), kompaktowe pliki źródłowe (JEP 512) i elastyczne konstruktory (JEP 513). Zawiera też istotne ulepszenia JVM (JEP 518–521) i rozbudowane API, a także funkcje preview i eksperymentalne, które mają kształtować przyszłość języka.

Czy nowe funkcje są wstecznie zgodne z poprzednimi wersjami Javy?

Tak, finalne JEP-y są wstecznie zgodne i gotowe do produkcji. JEP-y w wersji preview lub eksperymentalne są opcjonalne i mogą wymagać zmian w kodzie lub specjalnych flag, by działać ze starszymi bazami kodu.

Czy korzystanie z JEP-ów preview lub inkubacyjnych jest bezpieczne?

Służą one do eksperymentów. Możesz je testować, by poznać nadchodzące funkcje, ale w kolejnych wydaniach mogą zostać zmienione lub usunięte, dlatego nie zalecamy ich w krytycznych środowiskach produkcyjnych.

Do czego przydaje się Module Import (JEP 511)?

Upraszcza instrukcje importu, zmniejszając liczbę linii potrzebnych do dołączenia całych modułów. Kod staje się dzięki temu wyraźnie czytelniejszy, bardziej przejrzysty i łatwiejszy w utrzymaniu.

Czy warto od razu przejść na Javę 25?

W projektach produkcyjnych zalecamy najpierw sprawdzić zgodność z Twoimi obecnymi zależnościami. Nowe funkcje można wdrażać stopniowo, zaczynając od finalnych JEP-ów, by zachować stabilność.

Jakie korzyści wydajnościowe JVM daje Java 25?

JVM zyskuje kilka optymalizacji, w tym Early Method Profiling (JEP 515), Cooperative JFR Sampling (JEP 518) i stabilny już Generational Shenandoah (JEP 521). Razem poprawiają wydajność aplikacji, efektywność pamięci i ogólną stabilność.

Gdzie znaleźć oficjalne materiały o Javie 25 i jej JEP-ach?

Szczegółowa dokumentacja wszystkich JEP-ów jest dostępna na openjdk.org, a kompilacje Javy 25 można pobrać ze strony jdk.java.net/25.