You are here
Home > java > Core Java >

Java 25 New Features With Examples

Java 25 New Features With Examples

Java 25 New Features With ExamplesJava 25 was officially released on September 16, 2025. It is a Long-Term Support (LTS) release that includes numerous enhancements across core Java libraries, language specifications, security, and performance. Oracle plans to provide support for Java 25 for at least eight years, allowing organizations to migrate at their own pace while benefiting from the latest features, including improved AI capabilities and enhanced developer productivity.

Java 25, the latest Long-Term Support (LTS) release offers major upgrades across language syntax, APIs, security, performance, and monitoring, making Java easier and more powerful for everyone. All new features are introduced via JDK Enhancement Proposals (JEPs), so each section lists the JEP, summarizes the upgrade, and provides practical code samples or use cases.

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

The goal of this feature is to enable uniform data exploration by allowing type patterns for all types, whether primitive or reference. This feature was originally proposed by JEP 455 (JDK 23) and re-previewed by JEP 488 (JDK 24), without change. In JDK 25, it is proposed as a preview feature for a third time, without change.

Java’s pattern matching now supports primitive types, streamlining code and reducing errors. This enables direct, type-safe matching and destructuring of primitives without unnecessary boxing or verbose code. Pattern cases for primitives simplify switch and instanceof. This feature promotes pattern matching for primitives more expressively:

Example#1:

Object obj = 42;
switch (obj) {
    case int i -> System.out.println("Primitive int: " + i);
    case double d -> System.out.println("Primitive double: " + d);
    default -> System.out.println("Something else");
}

Example#2:

Integer code=-1;
switch (code) {
    case int n when n > 0 -> System.out.println("Positive int: " + n);
    case int n when n < 0 -> System.out.println("Negative int: " + n);
    default -> System.out.println("Zero");
}

This avoids unnecessary casting, boxing/unboxing and makes the code much cleaner.

Example#3:

Object obj = 10;
if (obj instanceof int val) {
System.out.println("Primitive int value: " + val);
} else if (obj instanceof double val) {
System.out.println("Primitive double value: " + val);
}

Here, Primitive pattern matching allows instanceof to be used not just for reference types, but also primitive types like int, double, etc. It removes the need for manual unboxing and type-casting boilerplate. In previous Java versions, developers would have to write explicit casting logic. If obj is compatible (e.g., a boxed Integer for int, or a Double for double), the match succeeds, and the primitive variable is directly usable. This approach improves type safety and readability when working with generically-typed or boxed values.

JEP 512: Compact Source Files and Instance Main Methods

This feature was first previewed in Java SE 21 as JEP 445: Unnamed Classes and Instance Main Methods (Preview) and previewed again in Java SE 22, 23, and 24, this feature is permanent in this release with a revised title ‘Compact Source Files and Instance Main Methods‘.

Following are the updates in this release:

  • A key change involves the package location of the basic console I/O class, IO, which is now part of the java.lang package. This placement means it is implicitly accessible to every Java source file.
  • For compact source files, the static methods from the IO class are no longer implicitly imported. Method calls must now be prefixed with the class name, as in IO.println(“Hello, world!”), unless a specific static import statement is used.
  • The implementation of the IO class has been refactored; it now functions as a wrapper for System.out and System.in, replacing its previous dependency on the java.io.Console class.

You may check the status of this feature (JEP-495) in Java 24 release.

JEP 513: Flexible Constructor Bodies

The feature was first previewed in Java SE 22 as JEP 447: Statements before super(…) (Preview), next previewed again in Java SE 23 and Java SE 24. Now this feature is permanent in this release without any significant changes.

This feature allows statements to run in the prologue, before the explicit constructor invocation (super(…) or this(…)). You can now validate inputs, perform calculations, or invoke helper methods; all before calling the superclass constructor. However, you still cannot reference the object under construction (no this, no accessing fields that aren’t yet initialized) in this context.

Key new capabilities:

  • Input validation before superclass construction.

  • Calculations or preparation of arguments passed to the superclass.

  • Cleaner, safer object construction, fail fast before expensive allocation.

Restrictions

  • You cannot read fields or call instance methods on this before constructor chaining.

  • Fields can be initialized, but you must not leak references to the partially constructed object.

  • Most static methods and local helpers are permitted in this pre-super region.

Example#1: Defensive Programming (Argument Validation):

public class PositiveBigInteger extends BigInteger {
   public PositiveBigInteger(long value) {
      if (value <= 0) throw new IllegalArgumentException("Value must be positive");
      super(Long.toString(value));
   }
}
Example#2: Conditional Super Constructor Calls:
public class CustomLogger extends Logger {
   public CustomLogger(String config) {
      String level = "INFO";
      if ("debug".equals(config)) {
        level = "DEBUG";
      }
      super(level); // can compute before super()
   }
}

Arguments are computed before superclass allocation.

Example#3: Helper Method Invocation:
public class Metric extends DataPoint {
   public Metric(String key) {
      String normalizedKey = normalizeKey(key);
      super(normalizedKey);
   }
   private static String normalizeKey(String key) {
     return key.trim().toLowerCase();
   }
}

Helpers can be run before superclass constructor.

Example#4: Record Constructors and JEP 513:
public record ValidatedPoint(int x, int y) {
    public ValidatedPoint(int x, int y) {
       checkNonNegative(x, y); // now allowed before this()
       this(x, y);
    }
    private static void checkNonNegative(int x, int y) {
       if (x < 0 || y < 0) throw new IllegalArgumentException();
    }
}

Non-canonical record constructors benefit from this flexibility.

Example#5: Enum Constructors:
public enum Status {
   ACTIVE("A"), INACTIVE("I");
   private final String code;

   Status(String code) {
      if (!code.matches("[AI]")) throw new IllegalArgumentException();
      this.code = code;
   }
}

Validation can now happen before field initialization.

JEP 511: Module Import Declarations

This feature was first previewed in Java SE 23, and previewed again in Java SE 24. It is now proposed to finalize in this release without any significant changes.

This feature introduces Module Import Declarations to the Java language, allowing developers to import all public types from all exported packages in a module with a single statement:

import module <ModuleName>;

With import module, bring all exported classes from a module in one step. The introduction of module import declarations aims to simplify software development in several key ways.

1) Streamline Library Usage: Enable the import of all types from a module with a single declaration, simplifying the use of modular libraries.

2) Reduce Import Redundancy: This reduces clutter by replacing multiple wildcard package imports with a single, concise module import declaration.

3) Lower the Learning Curve: Make it easier for newcomers to utilize essential Java classes and third-party libraries without first needing to know their specific package locations.

4) Maintain Compatibility: Ensure that new module import declarations can coexist without conflict alongside traditional package import statements.

5) Support Incremental Adoption: Allow developers to benefit from module imports even if their own codebase is not yet fully modularized.

Examples and further explanations of this feature is already discussed in detail in Module Import Declarations (JEP-494, Second Preview) of Java 24. 

  • How it works?
  • How to handle ambiguity while using Module import statements?
  • Transitive dependency in Java Module System
  • Implicit Availability of java.base
  • Explicitly Requiring java.se

JEP 505: Structured Concurrency (Fifth Preview)

What is Structured Concurrency?

Structured concurrency is a paradigm that treats related concurrent tasks as a single unit of work, with a well-defined dynamic scope. The principal API in Java is StructuredTaskScope, which ensures that:

  • The lifetime of subtasks is bounded by the lexical scope of the block (usually a try-with-resources statement).

  • Error handling, result collection, and cancellation are all coordinated in a clear way.

  • Observability and propagation of interruption are improved.

This contrasts with “unstructured” thread management (e.g., ExecutorService, CompletableFuture), where task lifecycles are often harder to track and errors/cancellations can silently propagate or be lost.

Why Use Structured Concurrency?

  • Simpler error handling: If any subtask fails, the rest are automatically canceled.

  • Clear lifecycle: All subtasks begin and end inside a single code block, preventing thread/resource leaks.

  • Coordinated cancellation: Parent scope interruptions propagate to all child tasks.

  • Clean composition: Results from all tasks are available or a collective failure is raised.

Progress of Structured Concurrency through several JDK releases

The development of Structured Concurrency has progressed through several JDK releases:

  • It started as an incubating feature in JDK 19 and JDK 20.

  • It moved to preview in JDK 21, where the fork method’s return type was changed from Future to Subtask.

  • It remained in preview through JDK 22, 23, and 24.

In the JDK 25 release,  proposed as another preview with key API improvements. Most notably, StructuredTaskScope objects will now be created using static factory methods. The basic  zero-parameter open() factory method serves the common use case of waiting for all subtasks to complete or for one to fail. For more complex policies, developers can use other factory methods that accept a Joiner to customize how results are handled.

Example:

If either findUser() or fetchOrder() fails (throws), the other task is canceled, and an error is propagated. All resources are guaranteed cleaned up upon leaving the try block.

Structured concurrency with JEP 505 enables safe, readable, and maintainable parallel code in Java by grouping subtasks into clear lifecycles and propagating errors/cancellations consistently. The StructuredTaskScope API is the core tool for building these patterns and makes Java concurrency code simpler and less error-prone.

JEP 506: Scoped Values

The scoped values API was proposed for incubation by JEP 429 (JDK 20), proposed for preview by JEP 446 (JDK 21), and subsequently improved and refined by JEP 464 (JDK 22), JEP 481 (JDK 23), and JEP 487 (JDK 24).

Now, it is proposed to finalize the scoped values API in JDK 25, with one small change: The ScopedValue.orElse method no longer accepts null as its argument.

What Are Scoped Values?

Scoped Values are a new concurrency primitive that allows code to share immutable context data between callers, callees, and child threads, within a well-defined lexical scope. Unlike ThreadLocal, their lifetime, accessibility, and mutation are all strictly bounded and managed by the language runtime.

Why Not ThreadLocal?

  • ThreadLocal lets you attach mutable context to a thread, but the context’s lifetime is unclear, risking memory leaks or stale data.

  • ThreadLocal values persist for the entire thread life unless explicitly removed, making them hazardous in environments using virtual threads, thread pools, or structured concurrency.

Scoped Values fix these issues:

  • Sessions/context are automatically inherited by child threads in a block.

  • No manual removal or cleanup needed. The value is accessible only inside the bound scope.

  • Values are truly immutable, preventing accidental sharing across requests.

How to Use Scoped Values?

Declaration: 

Scoped Values are typically declared as static final (like ThreadLocal), but they can only be read or bound inside a specific scope.

private static final ScopedValue<UserContext> CTX = ScopedValue.newInstance();

Binding a Value to a Scope:

Use the ScopedValue.where() method and run your logic within a lambda.

ScopedValue.where(CTX, userContext)
    .run(() -> {
        processRequest();
    });

Any code in processRequest including child threads or methods can access CTX.get() for that scope. Outside the scope, CTX.get() throws.

Example: Context Propagation in a Web Framework

Before scoped values (using ThreadLocal):

private static final ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();

void serve(Request request, Response response) {
    CONTEXT.set(createContext(request));
    Application.handle(request, response);
    CONTEXT.remove();
}

Risks include forgotten cleanup, leaking context, and confusing lifetimes.

With Scoped Values:

private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();

void serve(Request request, Response response) {
    var context = createContext(request);

    ScopedValue.where(CONTEXT, context).run(() -> {
        Application.handle(request, response);
    });
}

public PersistedObject readKey(String key) {
    var context = CONTEXT.get(); // Read within scope
    var db = getDBConnection(context);
    return db.readKey(key);
}
  • The value bound in serve() is only readable inside the .run() block and any method invoked within that call stack.

  • If readKey() is called after .run() returns, CONTEXT.get() will throw.

  • No need to remove the value manually.

Example Demonstrating the Change in ScopedValue.orElse() method

Example Throwing NullPointerException

 

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

    public static void main(String[] args) {
        // No binding for SCOPE in this thread
        // This would previously be legal in preview, but now will throw NullPointerException
        String fallback = null;
        String value = null;
        try {
            value = SCOPE.orElse(fallback); // Throws NullPointerException in Java 25
        } catch (NullPointerException e) {
            System.out.println("Caught expected NullPointerException: fallback cannot be null.");
        }

        // Correct usage: always supply a non-null fallback
        String safeValue = SCOPE.orElse("Default");
        System.out.println("Safe fallback: " + safeValue);
    }
}

The first call to SCOPE.orElse(null) will throw a NullPointerException in Java 25. The correct usage is to always supply a non-null fallback, as in SCOPE.orElse(“Default”). This API change ensures safety and consistency, so you should always provide a non-null fallback when using ScopedValue.orElse in Java 25. passed, it will throw a NullPointerException instead of returning null.

Correct Example:

private static final ScopedValue<String> SCOPED_USER = ScopedValue.newInstance();

public static void main(String[] args) {
    // No binding exists for SCOPED_USER here
    String result = SCOPED_USER.orElse("guest"); // "guest" is valid fallback
    System.out.println(result); // Prints: guest
}

JEP 502: Stable Values (Preview)

This JEP proposes a preview API for “stable values.” Stable Values are a Java 25 core-libs feature providing a new API for deferred immutability.

  • What it is: A new API for creating “stable values”, objects with data that does not change.

  • Performance: The JVM optimizes them just like final fields because their values are constant.

  • Benefit: They offer more flexibility than final fields in how and when you assign their value.

  • Purpose: To improve Java application startup performance by breaking down the large, single-block initialization of an app’s data into smaller, more efficient phases.
  • Status: This is a preview API in this release.

Why Stable Values?

  • Traditional final fields must be initialized eagerly (during construction or static initialization), which can slow down startup for large applications.

  • Mutable fields allow lazy initialization but lack JVM optimizations, and can lead to bugs from multiple or unsafe assignments.

  • Stable Values solve both issues: you can set the value later, only once, and still benefit from constant-folding and thread-safety.

Key API

  • StableValue<T> is the primary class.

  • StableValue.of() creates a new empty stable value.

  • .orElseSet(Supplier<T>) atomically sets the value if absent (lazily), guaranteeing that only one thread succeeds.

Example:

import java.lang.StableValue;

public class OrderController {
    // Deferred, immutable Logger—thread-safe, only set once!
    private final StableValue<Logger> logger = StableValue.of();

    public Logger getLogger() {
        // If not already set, safely initialize it (lazily)
        return logger.orElseSet(() -> Logger.create(OrderController.class));
    }
}

First call sets the value using the supplier, future calls return the immutable Logger.

Comparison with Lazy Initialization

Old way (before Stable Value):

private Logger logger = null;

Logger getLogger() {
    if (logger == null) {
        logger = Logger.create(OrderController.class);
    }
    return logger;
}

Not thread-safe, logger can be reassigned, no JVM constant-folding.

With Stable Values (Java 25):

private final StableValue<Logger> logger = StableValue.of();

Logger getLogger() {
    return logger.orElseSet(() -> Logger.create(OrderController.class));
}

Thread-safe, only assigned once, JVM treats as constant after initialization.

Benefits and Best Practices

  • Performance: Use deferred initialization to improve application startup, no need to build all objects at once.

  • Safety: StableValue guarantees at-most-once assignment, even across concurrent threads.

  • Clarity: API makes it clear which fields are immutable but lazily initialized.

  • Interoperability: JVM applies constant-folding optimizations for runtime usage, as it does for finals.

JEP 510: Key Derivation Function API

This introduces a standard API for Key Derivation Functions (KDFs). These are cryptographic tools used to generate new, secure keys from an existing secret key and additional data.

This KDF API was first introduced as a preview feature in JDK 24 (via JEP 478). The API is now proposed for finalization as a permanent feature in JDK 25, with no modifications from its preview version.

What is a Key Derivation Function (KDF)?

A Key Derivation Function is a cryptographic primitive that derives one or more secret keys from a primary secret (such as a password, master key, or shared secret). KDFs are essential in protocols like TLS, secure password storage, and post-quantum cryptography schemes. They take inputs such as initial key material, salt, and optional context info to generate cryptographically strong keys.

Example:

KeyDerivationFunction kdf = KeyDerivationFunction.create("HKDF");
SecretKey derived = kdf.deriveKey(originalKey, salt, info);

JEP 470: PEM Encodings of Cryptographic Objects (Preview)

This proposes a new preview API for working with the Privacy-Enhanced Mail (PEM) format. It will allow developers to encode security objects (like keys and certificates) into PEM text, and to decode PEM text back into their corresponding Java objects.

Primary purpose of this feature is to create a simple and concise API to handle the translation between PEM-encoded data and the objects they represent, and making the process much easier for developers.

What is PEM?

Privacy-Enhanced Mail (PEM) is a widely used text-based format for encoding cryptographic objects such as certificates, public keys, and private keys. PEM files are Base64-encoded binary data wrapped with human-readable header and footer lines like:

-----BEGIN CERTIFICATE-----
...Base64 encoded data...
-----END CERTIFICATE-----

PEM is the standard format used extensively in TLS/SSL certificates, SSH keys, and many security tools.

JEP 508: Vector API (Tenth Incubator)

JEP 508’s Vector API in Java 25 (tenth incubator) is an evolving and powerful API that lets Java developers write high-performance, portable, and readable vectorized code leveraging SIMD hardware capabilities, improving computational speed in many domains. This results in significantly better performance compared to traditional scalar (non-vector) code.

The Vector API has been developed as an incubating feature over many JDK releases, starting with JDK 16. It has been updated in each subsequent release through JDK 24.

It is now being proposed to continue its incubation in JDK 25 with several key improvements:

  1. API Enhancement: VectorShuffle can now work directly with data in off-heap memory (MemorySegment).

  2. Improved Maintainability: The internal implementation now uses the standard Foreign Function & Memory API to call math libraries, replacing complex custom code within the JVM. This makes the code easier to maintain.

  3. New Hardware Support: Operations on Float16 values (like addition and multiplication) are now automatically optimized using vector instructions on compatible x64 CPUs.

The API will remain incubating until foundational features from Project Valhalla are available. Once they are, the Vector API will be adapted to use them and will then move to the preview stage.

JEP 519: Compact Object Headers

Compact object headers were first introduced in JDK 24 as an experimental alternative to the standard object-header layout. This cautious approach is standard for large features, allowing for thorough testing.

JEP 519 makes compact object headers a first-class, production-level feature in Java 25, improving both memory use and runtime performance for Java applications, especially those with many small objects. This enhancement aligns with the goals of Project Lilliput, which aims to drastically reduce Java memory footprint.

Object headers shrink from 128 bits to 64 bits on 64-bit JVMs, reducing memory and improving performance especially for deployments with many small objects.

JEP 521: Generational Shenandoah

Generational Shenandoah was introduced experimentally in JDK 24 (JEP 404). JDK 25 promotes it to a product-level feature, removing the need for experimental JVM flags.

Shenandoah is a low-pause, concurrent garbage collector designed to minimize the traditional “stop-the-world” pauses by performing most of its work concurrently with running Java threads. It targets large heaps and latency-sensitive applications with pause times consistently under 10 ms.

Generational Shenandoah extends Shenandoah by introducing generational garbage collection support, segregating heap objects by age:

  • Young Generation: Recently created objects that tend to become unreachable quickly.

  • Old Generation: Long-lived objects that survive multiple collections.

This follows the weak generational hypothesis, which states that most objects die young.

An upgraded garbage collector offering lower pause times and better memory management.

Useful for: large scale, latency-sensitive applications.

JEP 514: Ahead-of-Time Command-Line Ergonomics

Ahead-of-Time compilation allows parts of Java bytecode to be compiled into native code prior to runtime. This reduces application startup time by avoiding some of the Just-In-Time (JIT) compilation work during application launch.

Java 24 introduced the ability to record an Ahead-of-Time (AOT) cache based on an application’s workload, enabling faster startup in subsequent runs by preloading and linking classes.

JEP 514 in Java 25 introduces the -XX:AOTCacheOutput command-line option, which combines both stages into a single JVM invocation that:

  • Runs the app once to record class loading behavior internally to a temporary file.
  • Creates the AOT cache file specified by the -XX:AOTCacheOutput option.
  • Cleans up temporary files automatically.

Example:

java -XX:AOTCacheOutput=app.aot -cp app.jar MainClass

Shortens a previously two-step process to just one command.

JEP 515: Ahead-of-Time Method Profiling

Ahead-of-Time Method Profiling is a feature introduced in Java 25 that collects method execution profiles during a training run of an application and stores this profile data in the AOT cache. This profile data includes statistics on method execution frequency, typical object types, and other runtime behavior that helps the JVM’s Just-In-Time (JIT) compiler optimize code more effectively and faster at startup.

Ahead-of-Time Method Profiling lets you bootstrap the JVM at startup with pre-existing execution profiles derived from earlier training runs, speeding up the hot-spotting and JIT optimization phases.

JEP 503: Remove the 32-bit x86 Port

JEP 503 removes the source code and build support for the 32-bit x86 (Intel/AMD) port of the HotSpot JVM in OpenJDK. This port was deprecated in JDK 24 (JEP 501) and fully removed in JDK 25.

What Was Removed?

  • Source files specific to the x86 32-bit architecture.

  • Build scripts and configurations to compile for 32-bit x86.

  • Tests exclusively run for 32-bit x86.

  • Compatibility and fallback code specific to this platform.

Platforms Still Supported

  • Other 32-bit platforms such as ARM32 remain supported.

  • 64-bit x86 (x86-64 / AMD64) remains the primary focus on Intel/AMD hardware.

  • Zero port (an architecture-agnostic interpreter mode) remains available but without JIT optimizations.

Impact on Developers

  • Applications running on 64-bit systems see no difference.

  • Legacy users running 32-bit x86 JVMs should consider migrating to 64-bit JVMs.

  • The Zero interpreter can still be used on unsupported hardware but with degraded performance.

JEP 509: JFR CPU-Time Profiling

Java Flight Recorder (JFR) is the JVM’s built-in profiling and diagnostics tool. Traditionally, JFR collects execution-time samples to approximate CPU usage, which samples stack traces at regular intervals of elapsed real time.

JEP 509 introduces an experimental CPU-time profiling feature specifically on Linux, capturing thread stack samples based on CPU-time consumed rather than elapsed time, providing much higher accuracy for CPU usage profiling.

JEP 518: JFR Cooperative Sampling

Java Flight Recorder (JFR) samples Java thread stacks asynchronously by suspending threads at arbitrary points to gather stack traces. This asynchronous sampling uses heuristics to walk thread stacks when threads are not at well-defined safe states.

JEP 518 redesigns the JFR sampling mechanism to improve stability and scalability by using cooperative sampling at JVM safepoints.

JEP 520: JFR Method Timing & Tracing

JFR (Java Flight Recorder) Method Timing & Tracing provides enhanced capabilities to precisely record execution times and traces of specific Java methods by instrumenting bytecode. These facilities improve on traditional sampling by providing exact, method-level statistics, including invocation counts and durations.

JEP 520 dramatically enhances JFR’s ability to provide detailed method-level timing and trace information to developers, enabling powerful performance insights and troubleshooting capabilities while minimizing application changes and overheads.

Complete Table of JEPs in Java 25 New Features With Examples

JEP Feature Status Category
470 PEM Encodings of Cryptographic Objects Preview Security
502 Stable Values Preview Core Libs
503 Remove the 32-bit x86 Port Final Removal
505 Structured Concurrency Fifth Preview Concurrency
506 Scoped Values Final Concurrency
507 Primitive Types in Patterns, instanceof, and switch Third Preview Syntax
508 Vector API Incubator Performance
509 JFR CPU-Time Profiling Experimental Profiling
510 Key Derivation Function API Final Security
511 Module Import Declarations Final Syntax
512 Compact Source Files and Instance Main Methods Final Syntax
513 Flexible Constructor Bodies Final Syntax
514 Ahead-of-Time Command-Line Ergonomics Final Performance
515 Ahead-of-Time Method Profiling Final Performance
518 JFR Cooperative Sampling Final Profiling
519 Compact Object Headers Final Performance
520 JFR Method Timing & Tracing Final Profiling
521 Generational Shenandoah Final Garbage Collector

Conclusion

Java 25’s improvements make learning and professional development easier and faster. The new compact programs help new learners start quickly, while enhancements in concurrency, performance, and security benefit experienced developers with more robust, scalable applications. For every feature, refer to its JEP if deeper technical details are needed. Java’s evolution continues at full speed, keeping it one of the most relevant programming languages in the world today.


For other version’s features, kindly go through Java Features After Java 8.

Also check ‘How to add JDK 25 in Eclipse or STS?‘ to write & test Java programs using Java 25.

Leave a Reply


Top