DEV Community

Cover image for The Return of the LTS: What Java 25 Brings Beyond 21
Petr Filaretov
Petr Filaretov

Posted on

The Return of the LTS: What Java 25 Brings Beyond 21

Intro

In one of my previous posts, I wrote about features between Java LTS versions 21 and 17. Now, two years later (how!? Two years already!?), a new LTS version is scheduled for release on September 16, 2025 — Java 25. Today, it has moved into the "Final Release Candidate" phase, and the feature set is frozen.

The vast majority of projects skip non-LTS releases and use Java LTS versions only. So, let's take a look at the features that the new LTS version (Java 25) delivers compared to the previous LTS version (Java 21).

I will be using the following abbreviations for the feature states in the tables below:

  • exp = Experimental
  • inc[2|3|4|etc] = Incubator [2|3|4|etc]
  • pre[2|3|4|etc] = Preview [2|3|4|etc]
  • prod = Production
  • w/d = Withdrawn

Production-Ready Features in Java 25

Feature Java 21 Java 22 Java 23 Java 24 Java 25
Scoped Values JEP 446 (pre) JEP 464 (pre2) JEP 481 (pre3) JEP 487 (pre4) JEP 506 (prod)
Flexible Constructor Bodies - JEP 447 (pre) JEP 482 (pre2) JEP 492 (pre3) JEP 513 (prod)
Unnamed Variables & Patterns JEP 443 (pre) JEP 456 (prod) + + +
Stream Gatherers - JEP 461 (pre) JEP 473 (pre2) JEP 485 (prod) +
Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism - - - JEP 496 (prod) +
Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm - - - JEP 497 (prod) +
Key Derivation Function API - - - JEP 478 (pre) JEP 510 (prod)
Foreign Function & Memory API JEP 442 (pre3) JEP 454 (prod) + + +
Compact Source Files and Instance Main Methods JEP 445 (pre) JEP 463 (pre2) JEP 477 (pre3) JEP 495 (pre4) JEP 512 (prod)
Markdown Documentation Comments - - JEP 467 (prod) + +
Module Import Declarations - - JEP 476 (pre) JEP 494 (pre2) JEP 511 (prod)
Ahead-of-Time Class Loading & Linking - - - JEP 483 (prod) +
Ahead-of-Time Command-Line Ergonomics - - - - JEP 514 (prod)
Ahead-of-Time Method Profiling - - - - JEP 515 (prod)
JFR Cooperative Sampling - - - - JEP 518 (prod)
JFR Method Timing & Tracing - - - - JEP 520 (prod)
Compact Object Headers - - - JEP 450 (exp) JEP 519 (prod)
Prepare to Restrict the Use of JNI - - - JEP 472 (prod) +
Region Pinning for G1 - JEP 423 (prod) + + +
Late Barrier Expansion for G1 - - - JEP 475 (prod) +
Class-File API - JEP 457 (pre) JEP 466 (pre2) JEP 484 (prod) +
Permanently Disable the Security Manager - - - JEP 486 (prod) +
Launch Multi-File Source-Code Programs - JEP 458 (prod) + + +
ZGC: Generational Mode by Default - - JEP 474 (prod) + +
ZGC: Remove the Non-Generational Mode - - - JEP 490 (prod) +
Generational Shenandoah - - - JEP 404 (exp) JEP 521 (prod)
Synchronize Virtual Threads without Pinning - - - JEP 491 (prod) +
Linking Run-Time Images without JMODs - - - JEP 493 (prod) +
Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal - - JEP 471 (prod) + +
Warn upon Use of Memory-Access Methods in sun.misc.Unsafe - - - JEP 498 (prod) +
Remove the Windows 32-bit x86 Port - - - JEP 479 (prod) +
Deprecate the 32-bit x86 Port for Removal - - - JEP 501 (prod) +
Remove the 32-bit x86 Port - - - - JEP 503 (prod)

Preview Features in Java 25

Feature Java 21 Java 22 Java 23 Java 24 Java 25
Stable Values - - - - JEP 502 (pre)
PEM Encodings of Cryptographic Objects - - - - JEP 470 (pre)
Structured Concurrency JEP 453 (pre) JEP 462 (pre2) JEP 480 (pre3) JEP 499 (pre4) JEP 505 (pre5)
Primitive Types in Patterns, instanceof, and switch - - JEP 455 (pre) JEP 488 (pre2) JEP 507 (pre3)

Incubator Features in Java 25

Feature Java 21 Java 22 Java 23 Java 24 Java 25
Vector API JEP 448 (inc6) JEP 460 (inc7) JEP 469 (inc8) JEP 489 (inc9) JEP 508 (inc10)

Experimental Features in Java 25

Feature Java 21 Java 22 Java 23 Java 24 Java 25
JFR CPU-Time Profiling - - - - JEP 509 (exp)

Withdrawn Features Between Java 21 and Java 25

Feature Java 21 Java 22 Java 23 Java 24 Java 25
String Templates JEP 430 (pre) JEP 459 (pre2) JEP 465 (pre3 - w/d) - -

Production-Ready Features with Examples

Now, let's briefly go through some production-ready features and look at code samples.

The full source code is available on GitHub: pfilaretov42/java-features.

Scoped Values

Scoped Values provide a safer alternative to thread-local variables by offering immutable, inheritable values that are only accessible within a defined scope.

Before: The ThreadLocal Approach

Like carrying the One Ring - dangerous if shared carelessly:

class ThreadLocalTest {
    private static final ThreadLocal<String> CURRENT_RING_BEARER = new ThreadLocal<>();

    void dangerousJourney() {
        CURRENT_RING_BEARER.set("Frodo");
        try {
            travelToMordor();
        } finally {
            CURRENT_RING_BEARER.remove();
        }
    }

    void travelToMordor() {
        // Any method in the call chain can access...
        String bearer = CURRENT_RING_BEARER.get();
        System.out.println(bearer + " bears the burden");

        // ...and modify
        CURRENT_RING_BEARER.set("Sam");
        bearer = CURRENT_RING_BEARER.get();
        System.out.println(bearer + " bears the burden");
    }

    public static void main() {
        ThreadLocalTest test = new ThreadLocalTest();
        test.dangerousJourney();
    }
}
Enter fullscreen mode Exit fullscreen mode

After: The ScopedValue Way

Like the Phial of Galadriel - light that's safely contained:

public class ScopedValuesTest {
    private static final ScopedValue<String> CURRENT_RING_BEARER = ScopedValue.newInstance();

    void safeJourney() {
        ScopedValue.where(CURRENT_RING_BEARER, "Frodo")
                .run(this::travelToMordorSafely);
    }

    private void travelToMordorSafely() {
        // Only accessible in this scope, cannot be modified
        String bearer = CURRENT_RING_BEARER.get();
        System.out.println(bearer + " bears the burden safely");

        // Attempting to rebind would result in compilation error:
        //currentRingBearer.set("Sam");
    }

    void noJourney() {
        // Not accessible outside the scope, throws NoSuchElementException (ScopedValue not bound):
        String bearer = CURRENT_RING_BEARER.get();
        System.out.println(bearer + " bears the burden");
    }

    public static void main() {
        ScopedValuesTest test = new ScopedValuesTest();
        test.safeJourney();
        test.noJourney();
    }
}
Enter fullscreen mode Exit fullscreen mode

Flexible Constructor Bodies

Allows more flexible ordering of statements in constructor bodies, including permitting code before explicit constructor invocations (this() or super() calls).

Before: The Rigid Constructor Rules

Like the rigid walls of Orthanc - no flexibility:

class Palantir {
    private final String owner;
    private final boolean isCorrupted;

    public Palantir(String owner) {
        // This would not compile:
        //validateOwner(owner);
        // Call to this() must be the first statement:
        this(validateOwner(owner), false);
    }

    private static String validateOwner(String owner) {
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("Owner cannot be null or blank");
        }
        System.out.println("A new palantir is given to " + owner);
        return owner;
    }

    private Palantir(String owner, boolean isCorrupted) {
        this.owner = owner;
        this.isCorrupted = isCorrupted;
    }
}

Enter fullscreen mode Exit fullscreen mode

After: The Graceful Constructor Freedom

Now flows like the waters of Bruinen - natural and flexible:

class Palantir {
    private final String owner;
    private final boolean isCorrupted;

    public Palantir(String owner) {
        validateOwner(owner);
        // Now this() call is allowed after other statements:
        this(owner, false);
    }

    private static void validateOwner(String owner) {
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("Owner cannot be null or blank");
        }
        System.out.println("A new palantir is given to " + owner);
    }

    private Palantir(String owner, boolean isCorrupted) {
        this.owner = owner;
        this.isCorrupted = isCorrupted;
    }
}
Enter fullscreen mode Exit fullscreen mode

Unnamed Variables & Patterns

Allows developers to explicitly mark unused variables/patterns with underscore _ to improve code clarity and maintainability.

Common Use Cases:

  • Exception parameters
  • Lambda parameters
  • Pattern matching variables
  • Loop variables when only counter is needed

Before: Forced to Name Unused Variables

Like being forced to name every orc in Mordor's army:

try {
    int rings = forgeNewRing();
} catch (RingForgingException e) {  // Never used
    System.out.println("The fires of Mount Doom failed us!");
}

// Pattern matching with unused bindings
if (fighter instanceof Elf(String name, Weapon(String type, int damage))) {  // name and damage unused
    System.out.println("Armed with: " + type);
}
Enter fullscreen mode Exit fullscreen mode

After: Clean Unused Variable Declaration

As elegant as Legolas defying gravity:

try {
    int _ = forgeNewRing();
} catch (RingForgingException _) {  // Clear this is unused
    System.out.println("The fires of Mount Doom failed us!");
}

// Clean pattern matching
if (fighter instanceof Elf(_, Weapon(String type, _))) {
    System.out.println("Armed with: " + type);
}
Enter fullscreen mode Exit fullscreen mode

Stream Gatherers

Introduces custom intermediate stream operations through the new gather(Gatherer) method, allowing more complex stream transformations.

Before: Limited to Built-in Operations

Like trying to forge the One Ring with only basic tools:

List<String> hobbits = List.of("Frodo", "Sam", "Merry", "Pippin");

// To get overlapping pairs, we needed a workarounds
List<String> pairs = IntStream.range(0, hobbits.size() - 1)
        .mapToObj(i -> hobbits.get(i) + " & " + hobbits.get(i + 1))
        .toList();
System.out.println(pairs);
// Output: ["Frodo & Sam", "Sam & Merry", "Merry & Pippin"]
Enter fullscreen mode Exit fullscreen mode

After: Custom Stream Transformations

Now wielding the power of the Elven smiths:

List<String> hobbits = List.of("Frodo", "Sam", "Merry", "Pippin");

Gatherer<String, ?, String> pairing = Gatherer.ofSequential(
        () -> new Object() { String previous; },
        (state, element, downstream) -> {
            if (state.previous != null) {
                downstream.push(state.previous + " & " + element);
            }
            state.previous = element;
            return true;
        }
);

List<String> pairs = hobbits.stream()
        .gather(pairing)
        .toList();
System.out.println(pairs);
// Output: ["Frodo & Sam", "Sam & Merry", "Merry & Pippin"]
Enter fullscreen mode Exit fullscreen mode

Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism

Java 24 introduces built-in support for module-lattice-based key encapsulation mechanism (ML-KEM), part of quantum-resistant cryptography.

This feature provides quantum-resistant key encapsulation that is used to secure symmetric keys over insecure communication channels using public key cryptography.

It can be used to withstand the might of quantum computers — future threats to classical RSA and Diffie-Hellman.

With the following session key exchange example...

// Elrond prepares his runes of protection (Receiver creates key pair)
ElrondTheReceiver elrond = new ElrondTheReceiver();

// Gandalf crafts a secret using Elrond’s rune (Sender uses receiver's public key to encapsulate session key)
GandalfTheSender gandalf = new GandalfTheSender(elrond.revealPublicRune());
SecretKey senderSessionKey = gandalf.getSessionKey();

// Elrond deciphers the sealed whisper from Gandalf (Receiver decapsulates to get the same session key)
SecretKey receiverSessionKey = elrond.decapsulateWhisper(gandalf.getSealedWhisper());
boolean secretsMatch = MessageDigest.isEqual(senderSessionKey.getEncoded(), receiverSessionKey.getEncoded());

// Output for verification:
HexFormat hex = HexFormat.of();
System.out.println("Sender session key:   " + hex.formatHex(senderSessionKey.getEncoded()));
System.out.println("Receiver session key: " + hex.formatHex(receiverSessionKey.getEncoded()));
System.out.println("Secrets match: " + secretsMatch);

if (secretsMatch) {
    // Gandalf and Elrond exchange messages using the securely transmitted session key
    // ...
}
Enter fullscreen mode Exit fullscreen mode

..., here is what we have.

Before: RSA Key Transport

Like wielding Gondorian steel — mighty, yet vulnerable to greater powers:

/**
 * Receiver generates key pair and decapsulates session key
 */
class ElrondTheReceiver {
    private final KeyPair keyPair;

    public ElrondTheReceiver() throws GeneralSecurityException {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(4096);
        keyPair = generator.generateKeyPair();
    }

    public PublicKey revealPublicRune() {
        return keyPair.getPublic();
    }

    public SecretKey decapsulateWhisper(byte[] sealedWhisper) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
        byte[] keyBytes = cipher.doFinal(sealedWhisper);

        return new SecretKeySpec(keyBytes, "AES");
    }
}
Enter fullscreen mode Exit fullscreen mode
/**
 * Sender uses receiver's public key to generate session key
 */
class GandalfTheSender {
    private final SecretKey sessionKey; // Sender’s secret session key
    private final byte[] sealedWhisper; // The message with session key encapsulated for Receiver

    public GandalfTheSender(PublicKey receiverPublicKey) throws GeneralSecurityException {
        // Generate session key (AES)
        KeyGenerator generator = KeyGenerator.getInstance("AES");
        generator.init(256);
        sessionKey = generator.generateKey();

        // Encrypt (encapsulate) session key with RSA
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, receiverPublicKey);
        sealedWhisper = cipher.doFinal(sessionKey.getEncoded());
    }

    public SecretKey getSessionKey() {
        return sessionKey;
    }

    public byte[] getSealedWhisper() {
        return sealedWhisper;
    }
}
Enter fullscreen mode Exit fullscreen mode

After: Post-Quantum Key Encapsulation

Like the Gates of Moria - impregnable to force:

/**
 * Receiver generates key pair and decapsulates session key
 */
class ElrondTheReceiver {
    private final KeyPair keyPair;

    public ElrondTheReceiver() throws GeneralSecurityException {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("ML-KEM");
        keyPair = generator.generateKeyPair();
    }

    public PublicKey revealPublicRune() {
        return keyPair.getPublic();
    }

    public SecretKey decapsulateWhisper(byte[] sealedWhisper) throws GeneralSecurityException {
        KEM kem = KEM.getInstance("ML-KEM");
        KEM.Decapsulator decapsulator = kem.newDecapsulator(keyPair.getPrivate());
        return decapsulator.decapsulate(sealedWhisper);
    }
}
Enter fullscreen mode Exit fullscreen mode
/**
 * Sender uses receiver's public key to generate session key
 */
class GandalfTheSender {
    private final SecretKey sessionKey; // Sender’s secret session key
    private final byte[] sealedWhisper; // The message with session key encapsulated for Receiver

    public GandalfTheSender(PublicKey receiverPublicKey) throws GeneralSecurityException {
        KEM kem = KEM.getInstance("ML-KEM");
        KEM.Encapsulator encapsulator = kem.newEncapsulator(receiverPublicKey);
        KEM.Encapsulated encapsulated = encapsulator.encapsulate();

        sessionKey = encapsulated.key();
        sealedWhisper = encapsulated.encapsulation();
    }

    public SecretKey getSessionKey() {
        return sessionKey;
    }

    public byte[] getSealedWhisper() {
        return sealedWhisper;
    }
}
Enter fullscreen mode Exit fullscreen mode

Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm

ML-DSA provides post-quantum secure digital signatures based on lattice cryptography, part of Java's new cryptographic suite for the quantum computing era.

With the following message signing example...

// Gandalf prepares his spell (Sender creates a public/private key pair)
GandalfTheSender gandalf = new GandalfTheSender();

// Sender signs a message using the private key
String scroll = gandalf.speakWordsOfPower();
byte[] waxSeal = gandalf.signMessage(scroll);

// Aragorn receives the scroll and the seal
AragornTheReceiver aragorn = new AragornTheReceiver(gandalf.revealPublicRune());

// Verifying the true words of Gandalf (Receiver verifies the message using the sender's public key)
boolean isTrueScroll = aragorn.verifyMessage(scroll, waxSeal);
System.out.println("Is the scroll valid: " + isTrueScroll); // true

// Attempt to fool the ranger with a forged scroll (verification fails for counterfeit message)
String fakeScroll = """
    A new Power is rising. Against it the old allies and policies will not avail us at all. \
    There is no hope left in Elves or dying Númenor.
    """;
isTrueScroll = aragorn.verifyMessage(fakeScroll, waxSeal);
System.out.println("Is the forged scroll valid: " + isTrueScroll); // false
Enter fullscreen mode Exit fullscreen mode

..., here is what we have.

Before: Signing with ECDSA

Like the walls of Isengard against Entish wrath:

/**
 * Sender signs message with private key
 */
class GandalfTheSender {
    private final KeyPair keyPair;

    public GandalfTheSender() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
        generator.initialize(new ECGenParameterSpec("secp256r1"));
        keyPair = generator.generateKeyPair();
    }

    public PublicKey revealPublicRune() {
        return keyPair.getPublic();
    }

    public byte[] signMessage(String message) throws Exception {
        Signature runeEngraver = Signature.getInstance("SHA256withECDSA");
        runeEngraver.initSign(keyPair.getPrivate());
        runeEngraver.update(message.getBytes());
        return runeEngraver.sign();
    }

    public String speakWordsOfPower() {
        return """
            It is not our part here to take thought only for a season, or for a few lives of Men, \
            or for a passing age of the world. We should seek a final end of this menace, \
            even if we do not hope to make one.
            """;
    }
}
Enter fullscreen mode Exit fullscreen mode
/**
 * Receiver verifies the message with public key
 */
class AragornTheReceiver {
    private final PublicKey senderPublicKey;

    public AragornTheReceiver(PublicKey senderPublicKey) {
        this.senderPublicKey = senderPublicKey;
    }

    public boolean verifyMessage(String message, byte[] signature) throws Exception {
        Signature runeChecker = Signature.getInstance("SHA256withECDSA");
        runeChecker.initVerify(senderPublicKey);
        runeChecker.update(message.getBytes());
        return runeChecker.verify(signature);
    }
}
Enter fullscreen mode Exit fullscreen mode

After: Signing with Quantum-Resistant ML-DSA

Like inscribing your mark in the timeless halls of Valinor.

/**
 * Sender signs message with private key
 */
class GandalfTheSender {
    private final KeyPair keyPair;

    public GandalfTheSender() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("ML-DSA");
        keyPair = generator.generateKeyPair();
    }

    public PublicKey revealPublicRune() {
        return keyPair.getPublic();
    }

    public byte[] signMessage(String message) throws Exception {
        Signature runeEngraver = Signature.getInstance("ML-DSA");
        runeEngraver.initSign(keyPair.getPrivate());
        runeEngraver.update(message.getBytes());
        return runeEngraver.sign();
    }

    public String speakWordsOfPower() {
        return """
            It is not our part here to take thought only for a season, or for a few lives of Men, \
            or for a passing age of the world. We should seek a final end of this menace, \
            even if we do not hope to make one.
            """;
    }
}
Enter fullscreen mode Exit fullscreen mode
/**
 * Receiver verifies the message with public key
 */
class AragornTheReceiver {
    private final PublicKey senderPublicKey;

    public AragornTheReceiver(PublicKey senderPublicKey) {
        this.senderPublicKey = senderPublicKey;
    }

    public boolean verifyMessage(String message, byte[] signature) throws Exception {
        Signature runeChecker = Signature.getInstance("ML-DSA");
        runeChecker.initVerify(senderPublicKey);
        runeChecker.update(message.getBytes());
        return runeChecker.verify(signature);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Derivation Function API

Introduces a standardised API for key derivation functions (KDFs), replacing ad-hoc implementations with a unified interface for secure key derivation.

Before: The Manual Key Derivation

Like forging a sword in the crude fires of Mordor:

String password = "Mellon!";
byte[] salt = generateSalt();
int iterations = 65_536;
int keyLengthBits = 256;

// derive the key
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, keyLengthBits);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKey key = factory.generateSecret(spec);

System.out.println("Derived key: " + HexFormat.of().formatHex(key.getEncoded()));
Enter fullscreen mode Exit fullscreen mode

After: The Refined KDF API

Now, like the Elven smiths of Eregion, we forge keys with precision:

KDF hkdf = KDF.getInstance("HKDF-SHA256");
byte[] password = "Mellon!".getBytes();
byte[] salt = generateSalt();
byte[] info = "Say 'Friend' and enter".getBytes();
int keyLengthBytes = 32;

// derive the key
AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract()
        .addIKM(password)
        .addSalt(salt)
        .thenExpand(info, keyLengthBytes);
SecretKey key = hkdf.deriveKey("AES", params);

System.out.println("Derived key: " + HexFormat.of().formatHex(key.getEncoded()));
Enter fullscreen mode Exit fullscreen mode

Foreign Function & Memory API

Replaces Java Native Interface (JNI) with a safer, more efficient way to call native code and work with off-heap memory.

I will skip package names and long file paths for simplicity. The fully working sample can be found on GitHub.

Before: The Risky JNI Approach

Like the Black Speech of Mordor - powerful but dangerous...

Create a Java class:

public class ElvenScroll {
    static {
        // Load the native library forged in C
        System.loadLibrary("mordor");
    }

    // Declare the native spell that reads the length of ancient text
    public native int countRunes(String ancientText);

    public static void main() {
        ElvenScroll scroll = new ElvenScroll();
        String runes = "Speak, friend, and enter";
        int length = scroll.countRunes(runes);
        System.out.println("Runes counted: " + length);
    }
}
Enter fullscreen mode Exit fullscreen mode

Compile the ElvenScroll class with -h option:

javac -h . ElvenScroll.java
Enter fullscreen mode Exit fullscreen mode

It produces the ElvenScroll.h header file.

Create a C file:

#include <jni.h>
#include <string.h>
#include "ElvenScroll.h"

JNIEXPORT jint JNICALL Java_ElvenScroll_countRunes(JNIEnv *env, jobject obj, jstring ancientText) {
    // Convert Elvish runes to C-compatible form
    const char *runes = (*env)->GetStringUTFChars(env, ancientText, NULL);
    if (runes == NULL) return 0;

    // Count the runes
    int length = (int)strlen(runes);

    // Release the spellbound memory
    (*env)->ReleaseStringUTFChars(env, ancientText, runes);
    return length;
}
Enter fullscreen mode Exit fullscreen mode

Build the C library - the command below is for macOS:

gcc -shared -fpic -o libmordor.dylib \
-I ${JAVA_HOME}/include \
-I ${JAVA_HOME}/include/darwin \
mordor.c
Enter fullscreen mode Exit fullscreen mode

This produces libmordor.dylib file.

Run the ElvenScroll class.

After: The Safe FFM API

Like the Elven bridges of Lothlórien - elegant and secure:

public class ElvenScroll {

    public static void main() throws Throwable {
        // Get a linker and a lookup object
        Linker linker = Linker.nativeLinker();
        SymbolLookup stdlib = linker.defaultLookup();

        // Get a handle to the foreign function ('strlen' from the C standard library)
        MethodHandle strlen = linker.downcallHandle(
            stdlib.find("strlen").orElseThrow(),
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        try (Arena arena = Arena.ofConfined()) {
            // Allocate off-heap memory
            String runes = "Speak, friend, and enter";
            MemorySegment cString = arena.allocateFrom(runes);

            // Call the foreign function
            long runeCount = (long) strlen.invoke(cString);
            System.out.println("Runes counted: " + runeCount);
        }
        // Memory automatically freed here - no memory leaks!
        // No native code!
    }
}
Enter fullscreen mode Exit fullscreen mode

Compact Source Files And Instance Main Methods

Simplifies Java syntax for beginners by allowing single-file programs without class boilerplate and offering more flexible main() method declarations.

Before: The Verbose Incantation

Like the long-winded speeches of the Council of Elrond:

package dev.pfilaretov42.java25.csf_imm;

public class RingQuestBefore {
    public static void main(String[] args) {
        System.out.println("One does not simply walk into Mordor...");
    }
}
Enter fullscreen mode Exit fullscreen mode

After: The Hobbit-Sized Version

A swift journey like Bilbo's contract:

// no package declaration
// no imports for classes in java.base module

void main() {
    IO.println("One does not simply walk into Mordor...");
}
Enter fullscreen mode Exit fullscreen mode

Or with an instance method:

package dev.pfilaretov42.java25.csf_imm;

public class RingQuestAfterInstance {
    void main() {
        IO.println("One does not simply walk into Mordor...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Markdown Documentation Comments

Finally! 🎉 Java now supports Markdown syntax in Javadoc comments, providing a more readable and maintainable alternative to HTML tags.

Before: The HTML Way

The way of the ancients — cluttered and burdensome, like the forges of Isengard:

/**
* <h1>The One Ring</h1>
* <p>Forged by Sauron in the fires of Mount Doom.</p>
* <p><b>Warning:</b> Do not wear for extended periods.</p>
*/
public class OneRing {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

After: Markdown Syntax

Now, the words flow like the elven script of Lothlórien — clean, elegant, and true:

/// # The One Ring
/// Forged by Sauron in the fires of Mount Doom.
/// **Warning:** Do not wear for extended periods.
public class OneRing {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Module Import Declarations

Introduces the import module syntax, allowing developers to import entire modules within regular Java files.

With the following classes...

package lothlorien;

public class Galadriel {
    public void speakLightOfEärendil() {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode
package lothlorien;

public class Celeborn {
    public void offerCounsel() {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode
package lothlorien;

public class Haldir {

    public void escort() {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

...and the module-info.java file...

module elves.of.lothlorien {
    exports lothlorien;
}
Enter fullscreen mode Exit fullscreen mode

..., here is what we have.

Before: Import Declarations

Like the endless genealogies of the House of Finwë:

import lothlorien.Celeborn;
import lothlorien.Galadriel;
import lothlorien.Haldir;

public class FarewellToLórien {
    void main() {
        Haldir guardian = new Haldir();
        guardian.escort();

        Celeborn lord = new Celeborn();
        lord.offerCounsel();

        Galadriel lady = new Galadriel();
        lady.speakLightOfEärendil();
    }
}
Enter fullscreen mode Exit fullscreen mode

After: Module Import

Now organized like a well-ordered dwarven hall:

import module elves.of.lothlorien;

public class FarewellToLórien {
    void main() {
        Haldir guardian = new Haldir();
        guardian.escort();

        Celeborn lord = new Celeborn();
        lord.offerCounsel();

        Galadriel lady = new Galadriel();
        lady.speakLightOfEärendil();
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Hey, who said Java is old and boring? The latest Java 25 LTS version delivers some great features that are definitely worth checking out.


Dream your code, code your dream.

Top comments (0)