← Back to Design & Development
Interview Prep

Java 25 Features
with Code Examples

LTS Release · September 2025 · 18 JEPs

01

Compact Source Files & Instance Main Methods

No more class wrapper or public static void main(String[] args). Write simple programs with minimal ceremony.

Write a Hello World program in Java 25 without a class declaration.
Before (Java 21)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
After (Java 25)
// That's the entire file!
void main() {
    println("Hello, World!");
}
Mention that this was inspired by languages like Python/Kotlin to lower the barrier for beginners. The class is implicitly created by the compiler.
02

Flexible Constructor Bodies

You can now execute statements before super() or this() — as long as you don't access this.

How does Java 25 change constructor chaining rules? Give an example.
Before (Java 21)
class Employee extends Person {
    Employee(String rawName) {
        // ❌ Cannot validate before super()
        // if (rawName == null) throw ...
        // Must call super() FIRST
        super(validate(rawName));
    }

    // Workaround: static helper method
    private static String validate(String n) {
        if (n == null) throw new
            IllegalArgumentException();
        return n.strip();
    }
}
After (Java 25)
class Employee extends Person {
    Employee(String rawName) {
        // ✅ Validate BEFORE super()
        if (rawName == null) {
            throw new
              IllegalArgumentException(
                "Name cannot be null"
              );
        }
        String cleaned = rawName.strip();

        // Now call super
        super(cleaned);
    }
}
Emphasize: you still cannot read/write instance fields or call instance methods before super(). Only local variables and static members are allowed in the "prologue" section.
03

Module Import Declarations

Import all exported packages of a module with a single statement. Drastically reduces import boilerplate.

What is a module import declaration and when would you use it?
Before (Java 21)
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

// 6 import statements just to get started
After (Java 25)
import module java.base;

// ✅ All public types from java.base
// are now available: List, Map, Path,
// Files, Stream, Collectors, etc.
Full Example
import module java.base;

void main() {
    List<String> names = List.of("Alice", "Bob", "Charlie");

    Map<Integer, List<String>> grouped = names.stream()
        .collect(Collectors.groupingBy(String::length));

    println(grouped);
    // {3=[Bob], 5=[Alice], 7=[Charlie]}
}
Mention that ambiguities (e.g., java.util.List vs. java.awt.List) are resolved by explicit single-type imports taking precedence.
04

Primitive Types in Pattern Matching

Pattern matching with instanceof and switch now supports all primitive types — no more manual casting.

Show how primitive pattern matching works in switch expressions.
Before (Java 21)
static String classify(Object obj) {
    if (obj instanceof Integer i) {
        return "Integer: " + i;
    } else if (obj instanceof Double d) {
        return "Double: " + d;
    }
    return "Unknown";
    // ❌ No primitive support
}
After (Java 25)
static String classify(Object obj) {
    return switch (obj) {
        case int i    -> "int: " + i;
        case double d -> "double: " + d;
        case long l  -> "long: " + l;
        default      -> "other";
    };
    // ✅ Primitives work directly!
}
instanceof with primitives
static void process(Object obj) {
    if (obj instanceof int i) {
        println("Got a primitive int: " + i);
    }

    // Also works with guards
    if (obj instanceof int i && i > 100) {
        println("Large int: " + i);
    }
}
This eliminates the asymmetry where reference types worked with pattern matching but primitives didn't. Mention it unifies the type system in pattern contexts.
05

Scoped Values (Finalized)

An immutable, thread-safe alternative to ThreadLocal — designed for virtual threads and structured concurrency.

Compare ScopedValue vs ThreadLocal. When would you choose one over the other?
ThreadLocal (Old Way)
// ❌ Mutable, leak-prone, per-thread copy
static final ThreadLocal<String> USER
    = new ThreadLocal<>();

void handleRequest() {
    USER.set("alice");
    try {
        doWork();
    } finally {
        USER.remove(); // easy to forget!
    }
}

void doWork() {
    String u = USER.get(); // "alice"
    USER.set("bob");  // ⚠ mutated!
}
ScopedValue (Java 25)
// ✅ Immutable, auto-cleaned, efficient
static final ScopedValue<String> USER
    = ScopedValue.newInstance();

void handleRequest() {
    ScopedValue
      .where(USER, "alice")
      .run(() -> doWork());
    // auto-cleaned when scope ends
}

void doWork() {
    String u = USER.get(); // "alice"
    // Cannot mutate! Immutable by design
}
Key interview points: ScopedValue is immutable (no set method), automatically scoped (no try/finally cleanup), inherited by child threads, and works efficiently with millions of virtual threads where ThreadLocal would be wasteful.
06

Structured Concurrency (Finalized)

Treat a group of concurrent subtasks as a single unit — if one fails, cancel the rest automatically.

Write code using StructuredTaskScope to fetch data from two services concurrently.
Before (Unstructured)
// ❌ Manual thread management
ExecutorService exec =
    Executors.newFixedThreadPool(2);

Future<User>  f1 = exec.submit(
    () -> fetchUser(id));
Future<Order> f2 = exec.submit(
    () -> fetchOrder(id));

// If fetchUser fails, fetchOrder
// keeps running wastefully
User  user  = f1.get();
Order order = f2.get();
exec.shutdown();
After (Java 25)
// ✅ Structured - auto-cancellation
Response handle(int id) throws Exception {
  try (var scope = new
      StructuredTaskScope
        .ShutdownOnFailure()) {

    var user  = scope.fork(
        () -> fetchUser(id));
    var order = scope.fork(
        () -> fetchOrder(id));

    scope.join()
         .throwIfFailed();

    return new Response(
        user.get(), order.get());
  }
  // If either fails, the other is
  // cancelled immediately!
}
Mention ShutdownOnFailure (fail-fast, cancel siblings) vs ShutdownOnSuccess (return first success, cancel rest). Both are built-in policies.
07

Compact Object Headers

Object headers shrunk from 96–128 bits to 64 bits. Huge memory savings for apps with millions of small objects.

Explain Compact Object Headers and its impact on memory usage.
Impact Illustration
// An empty Object in memory:

// Java 21 (default):  128 bits header = 16 bytes
//   [ Mark Word (64 bits) | Klass Pointer (64 bits) ]

// Java 25 (compact):   64 bits header =  8 bytes
//   [ Combined Mark + Klass (64 bits) ]

// Real-world impact example:
// 10 million small objects (e.g., records in a cache)
//   Before: 10M × 16 bytes = 160 MB in headers alone
//   After:  10M ×  8 bytes =  80 MB in headers
//   Savings: 80 MB → ~50% reduction!

// Enable at JVM level:
// java -XX:+UseCompactObjectHeaders MyApp
Originated from Project Lilliput. Mention that this is now a product-level feature (not experimental). Particularly beneficial for data-heavy applications, caches, and microservices with many short-lived objects.
08

Key Derivation Function API (KDF)

A new standard API for deriving cryptographic keys — needed for TLS, password hashing, and modern security protocols.

What is the Key Derivation Function API and why was it added?
Usage Example
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;

void deriveKey() throws Exception {
    // Create a KDF instance using HKDF-SHA256
    KDF kdf = KDF.getInstance("HKDF-SHA256");

    // Define extraction parameters
    byte[] salt = "random-salt".getBytes();
    byte[] ikm  = "input-key-material".getBytes();
    byte[] info = "context-info".getBytes();

    // Extract-then-Expand in one step
    HKDFParameterSpec params =
        HKDFParameterSpec
            .extractThenExpand(salt, ikm, info, 32);

    // Derive a 256-bit AES key
    SecretKey derivedKey =
        kdf.deriveKey("AES", params);

    println("Key algo: " + derivedKey.getAlgorithm());
    // Output: Key algo: AES
}
Before Java 25, developers relied on third-party libraries (like Bouncy Castle) for KDF. Now it's part of the standard library. Mention HKDF is used in TLS 1.3, Signal Protocol, and many modern crypto protocols.

Did this Java 25 cheat sheet help? If it did, tap the ❤️ — that's how I know it landed.