Post

What’s new in Java 25 (for Developers)

What’s new in Java 25 (for Developers)

Java 25 lands with a strong mix of language quality-of-life, better startup, powerful profiling, and practical APIs.

This post is a hands-on tour: short sections, runnable code, and concrete scenarios where you’ll use each feature soon. Each snippet is intentionally minimal so you can copy, adapt, and run quickly.

Legend

  • Final: stable, production-ready
  • 🧪 Preview: requires --enable-preview

🚀 Java 25 highlights

  • ✅ Flexible constructor bodies (JEP 513)
  • 🧩 Primitive patterns in instanceof and switch (JEP 507)
  • 📦 Compact programs & module imports (JEP 512 + JEP 511)
  • 🔄 Structured concurrency + scoped values (JEP 505 + JEP 506)
  • 🔐 KDF API and PEM encoding (JEP 510 + JEP 470)
  • 📊 New JFR profiling tools (JEP 520 + JEP 509)
  • ⚡ Faster startup via AOT cache (JEP 514 + JEP 515)

Language & syntax upgrades

JEP 513: Flexible constructor bodies (✅ final)

👉 JEP 513

What it is: You can run statements before calling super(...) or this(...). A new constructor “prologue” lets you validate inputs and initialize your own fields safely before delegating up the hierarchy.

When you’ll use it: Fail-fast validation and establishing subclass integrity before a superclass might call overridable methods.

Before (previous approach: had to call super first, often pushing validation into a factory or doing it after super, risking partially-initialized state if superclass used overridable methods):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// BEFORE (pre-Java 25)
class Person {
    Person(String name) { init(); }
    void init() { /* might call overridable methods */ }
}

class Employee extends Person {
    private String payrollId; // cannot set before super

    Employee(String name, String payrollId) {
        super(name); // must be first line
        if (payrollId == null || !payrollId.matches("EMP-\\d{4}")) {
            throw new IllegalArgumentException("Invalid payrollId");
        }
        this.payrollId = payrollId; // set only after super
    }
}

Limitations before:

  • Validation runs only after super, so superclass code might observe incomplete subclass state.
  • Workarounds: static factories, two-phase init, or duplicated validation earlier.

After (Employee payroll ID validated and initialized before Person’s constructor):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/main/java/com/loiane/people/Employee.java
package com.loiane.people;

class Person {
    final String name;
    Person(String name) { this.name = name; log("constructed: " + name); }
    void log(String msg) { System.out.println(msg); }
}

class Employee extends Person {
    final String payrollId;

    Employee(String name, String payrollId) {
        // Prologue: validate & set subclass state before calling super
        if (payrollId == null || !payrollId.matches("EMP-\\d{4}"))
            throw new IllegalArgumentException("Invalid payrollId");
        this.payrollId = payrollId; // allowed before super
        super(name); // delegation after establishing invariants
        log("ready: " + this.payrollId);
    }

    public static void main(String[] args) {
        new Employee("Ana", "EMP-1234");
    }
}

What’s happening here?

  • Validation + invariant setup happen in the prologue (before super).
  • Field assignment to payrollId is allowed early.
  • Subclass state exists before superclass logic runs.

JEP 507: Primitive patterns in instanceof and switch (🧪 third preview)

👉 JEP 507

What it is: Pattern matching now handles primitive types in instanceof and switch. You can safely test and bind narrowed values without manual range checks; switch supports boolean, long, float, and double too.

When you’ll use it: Safer numeric conversions, expressive parsing, and cleaner business rules.

Before (manual casting + separate branches: more boilerplate and risk of accidental widening):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BEFORE (pre-Java 25)
public class TierLegacy {
    static String tierFor(Object usage) {
        if (usage instanceof Integer) {
            int i = (Integer) usage;
            if (i < 1) return "FREE";
            if (i < 1_000) return "STARTER";
            if (i < 10_000) return "PRO";
            return "ENTERPRISE";
        } else if (usage instanceof Long) {
            long l = (Long) usage;
            if (l < 10_000) return "PRO"; else return "ENTERPRISE";
        } else if (usage instanceof Double) {
            double d = (Double) usage;
            int asInt = (int) d; // silent truncation risk
            if (asInt == d) {
                return tierFor(asInt); // re-box to Integer call
            }
            return d < 1_000.0 ? "STARTER" : "PRO";
        }
        return "UNKNOWN";
    }
}

Limitations before:

  • Repetition of casts and variable declarations.
  • Potential silent truncation when narrowing double to int.
  • No pattern guard inline with switch expression.

After (pricing tiers with precise narrowing and guards):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/main/java/com/loiane/billing/Tier.java
package com.loiane.billing;

public class Tier {
    static String tierFor(Object usage) {
        if (usage instanceof int i) {
            return switch (i) {
                case int v when v < 1 -> "FREE";
                case int v when v < 1_000 -> "STARTER";
                case int v when v < 10_000 -> "PRO";
                case int v -> "ENTERPRISE";
            };
        }
        if (usage instanceof long l) {
            return switch (l) {
                case long v when v < 10_000 -> "PRO";
                case long v -> "ENTERPRISE";
            };
        }
        if (usage instanceof double d) {
            // Bind only when d converts exactly to int (no silent truncation)
            if (d instanceof int exact) {
                return tierFor(exact);
            }
            return d < 1_000.0 ? "STARTER" : "PRO";
        }
        return "UNKNOWN";
    }

    public static void main(String[] args) {
        System.out.println(tierFor(0));        // FREE
        System.out.println(tierFor(999));      // STARTER
        System.out.println(tierFor(9_999));    // PRO
        System.out.println(tierFor(10_000L));  // ENTERPRISE (long path)
        System.out.println(tierFor(42.0));     // STARTER (double exact -> int)
    }
}

What’s happening here?

  • instanceof int exact binds only on exact conversion (no silent loss).
  • switch over long and guards on values make intent clear.

How to run (preview):

1
2
javac --enable-preview --release 25 -d out src/main/java/com/loiane/billing/Tier.java
java --enable-preview -cp out com.loiane.billing.Tier

Compact programs and cleaner imports

JEP 512: Compact source files + instance main (✅ final)

👉 JEP 512

What it is: You can write a file with just methods/fields; the compiler wraps it in an implicit class. void main() can be an instance method. New java.lang.IO makes console I/O easy.

When you’ll use it: Katas, scripts, teaching, quick demos.

Before (explicit class + static main required):

1
2
3
4
5
6
7
// BEFORE (pre-Java 25)
public class HelloClassic {

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

After (simplified compact source Hello World):

1
2
3
4
// Name this file: Hello.java (compact source)
void main() {
    IO.println("Hello, World!");
}

What’s happening here?

  • No class declaration; void main() is the entry point.
  • IO.println (java.lang.IO) avoids boilerplate.

Run:

1
java Hello.java

JEP 511: Module import declarations (✅ final)

👉 JEP 511

What it is: import module <name>; imports all exported packages of a module (and transitives), even from classpath code.

When you’ll use it: Prototyping and learning; quickly pulling in broad APIs (e.g., java.se in modular code).

Code (HTTP + JSON parsing with fewer imports):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/main/java/com/loiane/http/FetchTodo.java
package com.loiane.http;

import module java.base;
import module java.net.http;

public class FetchTodo {
    public static void main(String[] args) throws Exception {
        var client = java.net.http.HttpClient.newHttpClient();
        var req = java.net.http.HttpRequest.newBuilder(URI.create("https://jsonplaceholder.typicode.com/todos/1")).build();
        var res = client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}

What’s happening here?

  • Two module imports bring in all needed java.net.http types; we still qualify types for clarity.
  • If an ambiguity arises, add a specific import to resolve it.

Concurrency and context

JEP 506 + JEP 505: Scoped values (✅ final) + Structured concurrency (🧪 fifth preview)

👉 JEP 505 👉 JEP 506

What it is: Scoped values are a safer, inheritable alternative to ThreadLocal. Structured concurrency gives a lifecycle for task groups.

When you’ll use it: Propagating request IDs, auth, or locale across virtual threads with clear cancellation.

Code (correlation-id across a task group):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/main/java/com/loiane/observability/Trace.java
package com.loiane.observability;

import java.lang.ScopedValue;
import java.util.concurrent.StructuredTaskScope; // preview

public class Trace {
    static final ScopedValue<String> CORR_ID = ScopedValue.newInstance();

    static void log(String msg) { System.out.println("[" + CORR_ID.orElse("-") + "] " + msg); }

    public static void main(String[] args) throws Exception {
        var id = java.util.UUID.randomUUID().toString();
        ScopedValue.where(CORR_ID, id).run(() -> {
            try (var scope = new StructuredTaskScope<Void>()) { // simplified example
                var a = scope.fork(() -> { log("load customer"); return null; });
                var b = scope.fork(() -> { log("load orders"); return null; });
                scope.join();
                log("done");
            }
        });
    }
}

What’s happening here?

  • ScopedValue.where(CORR_ID, id).run(...) binds the correlation ID for the whole task scope.
  • Virtual threads (if used) inherit the scoped value without leaks.

Run (preview for structured concurrency):

1
2
javac --enable-preview --release 25 -d out src/main/java/com/loiane/observability/Trace.java
java --enable-preview -cp out com.loiane.observability.Trace

Security building blocks

JEP 510: KDF API (✅ final) with HKDF

👉 JEP 510

What it is: Standard javax.crypto.KDF for deriving keys; includes HKDF and parameter specs.

When you’ll use it: Session keys, HPKE/KEM flows, keys-from-seed.

Code (derive AES key material for an export):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/main/java/com/loiane/crypto/HkdfDemo.java
package com.loiane.crypto;

import javax.crypto.*;
import javax.crypto.spec.HKDFParameterSpec;
import java.security.spec.AlgorithmParameterSpec;

public class HkdfDemo {
    public static void main(String[] args) throws Exception {
        byte[] ikm = "seed-key-material".getBytes();
        byte[] salt = "salt".getBytes();
        byte[] info = "export-label".getBytes();

        KDF hkdf = KDF.getInstance("HKDF-SHA256");
        AlgorithmParameterSpec params = HKDFParameterSpec
                .ofExtract().addIKM(ikm).addSalt(salt)
                .thenExpand(info, 32);

        SecretKey key = hkdf.deriveKey("AES", params);
        System.out.println("Key algorithm: " + key.getAlgorithm());
        System.out.println("Key length: " + key.getEncoded().length);
    }
}

JEP 470: PEM encodings (🧪 preview)

👉 JEP 470

What it is: First-class APIs to read/write PEM and DER with optional encryption.

When you’ll use it: Import/export keys without third-party libs; cleanly handle PEM envelopes.

Code (write a simple PEM record of bytes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/main/java/com/loiane/crypto/PemWrite.java
package com.loiane.crypto;

import java.nio.file.*;
import java.util.HexFormat;
import jdk.security.pem.*; // preview API

public class PemWrite {
    public static void main(String[] args) throws Exception {
        byte[] payload = HexFormat.of().parseHex("DEADBEEF");
        var rec = PEMEncoder.encode("TEST-DATA", payload);
        Files.writeString(Path.of("test-data.pem"), rec);
    }
}

Run (preview):

1
2
javac --enable-preview --release 25 -d out src/main/java/com/loiane/crypto/*.java
java --enable-preview -cp out com.loiane.crypto.PemWrite

Profiling and observability

JEP 520: JFR method timing & tracing (✅ final)

👉 JEP 520

What it is: Time and trace specific methods by bytecode instrumentation: no code changes needed. Configure via CLI, config files, or JMX.

When you’ll use it: Pinpoint slow initializers, hotspots in frameworks, or leaks in resource lifecycles.

Try it (trace HashMap::resize while running your app):

1
2
java '-XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr' -jar yourapp.jar
jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr

Programmatic JMX (remote on-demand tracing) is also supported.

JEP 509: JFR CPU-time profiling (experimental, Linux)

👉 JEP 509

What it is: Accurate CPU-time profiles using the Linux CPU timer; attributes native time back to Java frames.

When you’ll use it: Production profiling with low overhead; prioritize CPU-bound work.

Quick start:

1
2
java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=cpu.jfr -jar yourapp.jar
jfr view cpu-time-hot-methods cpu.jfr

Startup and warmup

JEP 514: One-step AOT cache creation (✅ final)

👉 JEP 514

What it is: -XX:AOTCacheOutput=app.aot runs a training invocation and creates the cache in one step. Use -XX:AOTCache=app.aot in production.

Scenario: CI step trains startup (typical HTTP route + deserialization), publishes the cache artifact. Production pods mount app.aot for consistently fast cold starts.

1
2
3
4
5
# Train and create cache (one-shot)
java -XX:AOTCacheOutput=app.aot -jar yourapp.jar --train

# Use cache in prod
java -XX:AOTCache=app.aot -jar yourapp.jar

JEP 515: Ahead-of-time method profiling (✅ final)

👉 JEP 515

What it is: Profiles from training are stored in the AOT cache so the JIT optimizes earlier on startup.

Tip: For split infra, set create-only options via JDK_AOT_VM_OPTIONS if training and creation happen on different instances.

Notes

  • Preview language features (primitive patterns, structured concurrency) require --enable-preview to compile and run.
  • JFR additions enable both sampling (CPU-time) and precise timing/tracing; combine with JMC for deep analysis.
  • Leyden AOT workflows (AOTCacheOutput + method profiling) improve cold starts and reduce warmup time.
  • Keep an eye on preview/incubator JEPs; details (method names, minor API shapes) can still shift slightly until final.

Use the features that fit your use case today, and keep the previews on your radar: they’re already useful and getting close to final.

References

Happy Coding!

This post is licensed under CC BY 4.0 by the author.
This site uses cookies. Please choose whether to accept analytics cookies. Privacy Policy