Just a Blog

17 ☕ JDK 25: Practical New Features You'll Actually Use

01 Oct 2025

JDK 25 is here with practical improvements that make everyday Java development smoother. Let’s look at the features you’ll actually use. In previous blog post I covered JDK 21, and now after a while, I am going to cover the most interesting features (for me) from JDK 21 to JDK 25. For detailed info you can check individual JEPs but I will cover it very practically with form of before and after such feature was introduced.

Primitive Types in Patterns (JEP 455)

Pattern matching now works with primitive types, eliminating awkward wrapper conversions.

Before JDK 25:

Object obj = 42;
if (obj instanceof Integer i) {
    int value = i.intValue(); // Wrapper dance
    System.out.println("Value: " + value);
}

With JDK 25:

Object obj = 42;
if (obj instanceof int value) {
    System.out.println("Value: " + value); // Direct!
}

This works in switch expressions too:

String describe(Object obj) {
    return switch (obj) {
        case int i -> "Integer: " + i;
        case long l -> "Long: " + l;
        case double d -> "Double: " + d;
        case String s -> "String: " + s;
        default -> "Unknown";
    };
}

Flexible Constructor Bodies (JEP 482)

You can now write statements before super() or this() calls, as long as you don’t reference instance state.

Before:

public class User {
    private final String name;

    public User(String input) {
        super(); // Must be first
        // Now validate
        if (input == null || input.isBlank()) {
            throw new IllegalArgumentException("Name required");
        }
        this.name = input.trim();
    }
}

Now:

public class User {
    private final String name;

    public User(String input) {
        // Validate BEFORE calling super
        if (input == null || input.isBlank()) {
            throw new IllegalArgumentException("Name required");
        }
        String cleaned = input.trim();
        super();
        this.name = cleaned;
    }
}

Perfect for validation, logging, or preparing arguments:

public class DatabaseConnection {
    private final String url;

    public DatabaseConnection(String host, int port) {
        // Prepare connection string first
        String connUrl = "jdbc:postgresql://" + host + ":" + port;
        System.out.println("Connecting to: " + connUrl);

        super();
        this.url = connUrl;
    }
}

Markdown in JavaDoc (JEP 467)

Write JavaDoc using Markdown instead of HTML tags.

Old way:

/**
 * Processes user data.
 * <p>
 * <b>Important:</b> This method validates input.
 * <ul>
 *   <li>Checks for null values</li>
 *   <li>Validates email format</li>
 * </ul>
 *
 * @param user the user to process
 * @return processed result
 */
public Result process(User user) { ... }

Markdown way:

/// Processes user data.
///
/// **Important:** This method validates input.
/// - Checks for null values
/// - Validates email format
///
/// @param user the user to process
/// @return processed result
public Result process(User user) { ... }

Use /// for Markdown JavaDoc. Mix with traditional /** */ as needed.

Virtual Thread Pinning Improvements (JEP 491)

Virtual threads revolutionized concurrency in Java, but they had a problem: pinning. When a virtual thread performed certain blocking operations (like synchronized blocks or native calls), it would “pin” to its carrier platform thread, blocking other virtual threads from using that carrier.

JDK 25 eliminates the most common cause of pinning: synchronized blocks and methods.

The Problem (Before JDK 25):

// This would pin the virtual thread!
synchronized (lock) {
    // Long-running I/O operation
    database.query("SELECT * FROM users");
}

When a virtual thread hit this synchronized block, it pinned to its carrier thread. If you had thousands of virtual threads doing this, you’d lose the scalability benefits carrier threads would get blocked.

The Solution (JDK 25):

Now synchronized no longer pins virtual threads. The JVM handles it efficiently under the hood, so your existing code just works better:

// In JDK 25, this no longer pins!
synchronized (lock) {
    database.query("SELECT * FROM users");
}

Practical Impact:

Before JDK 25, you had to rewrite synchronized code to use ReentrantLock:

// Old workaround to avoid pinning
private final Lock lock = new ReentrantLock();

void process() {
    lock.lock();
    try {
        database.query("SELECT * FROM users");
    } finally {
        lock.unlock();
    }
}

Now you can keep your simpler synchronized code and still get full virtual thread benefits. Legacy code and libraries using synchronized automatically become virtual-thread-friendly.

Monitoring Pinning:

You can still detect remaining pinning issues (like JNI calls) with this JVM flag:

java -Djdk.tracePinnedThreads=full MyApp

This prints stack traces when virtual threads pin, helping you identify problematic code paths.

Class-File API (JEP 484)

A modern API for reading, writing, and transforming class files—replacing ASM and ByteBuddy for many use cases.

Reading class metadata:

import java.lang.classfile.*;

ClassModel cm = ClassFile.of().parse(Path.of("MyClass.class"));
System.out.println("Class: " + cm.thisClass().name());

for (MethodModel method : cm.methods()) {
    System.out.println("Method: " + method.methodName());
}

Transforming bytecode:

byte[] transformed = ClassFile.of().transform(
    ClassFile.of().parse(originalBytes),
    (builder, element) -> {
        if (element instanceof MethodModel mm &&
            mm.methodName().equalsString("oldName")) {
            builder.withMethod("newName", mm.methodType(),
                mb -> mm.code().ifPresent(mb::withCode));
        } else {
            builder.with(element);
        }
    }
);

This is a preview feature—useful for frameworks, bytecode instrumentation, and tooling.

Why This Matters

These features reduce boilerplate, improve readability, and make Java feel more modern:

  • Primitive patterns: Less wrapper noise
  • Flexible constructors: Validation where it belongs
  • Markdown JavaDoc: Documentation that’s easier to write and read
  • Virtual thread pinning fix: Legacy synchronized code now scales with virtual threads
  • Class-File API: Standard way to manipulate bytecode

JDK 25 continues Java’s evolution toward simplicity without sacrificing power. Upgrade and enjoy the improvements.