Java 25, released on September 16, 2025, is the latest Long-Term Support (LTS) release from Oracle. It follows Java 21 as the next LTS milestone and brings a collection of features that simplify everyday coding, modernize concurrency, and deliver meaningful performance gains. Whether you are upgrading from Java 17 or Java 21, this guide walks through every major feature with practical code examples you can try today.
Compact Source Files and Instance Main Methods (JEP 512)
One of the most celebrated changes in Java 25 is the finalization of Compact Source Files. Previously, even a simple “Hello, World!” program required a class declaration, a public static void main(String[] args) signature, and often an import or two. JEP 512 removes that ceremony: you can now write an instance main method without an enclosing class, making Java friendlier for scripting, teaching, and quick prototyping.
The IO class for basic console I/O is now in java.lang and implicitly imported, so reading and writing to the console no longer needs System.out.println.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}void main() {
IO.println("Hello, World!");
}The compiler wraps this in an implicit class automatically. You still have access to the full language — imports, methods, fields — but the entry point is dramatically simpler. This is particularly useful when teaching Java to beginners, who can focus on logic rather than syntax scaffolding.
Flexible Constructor Bodies (JEP 513)
Before Java 25, every statement in a constructor had to appear after the call to super() or this(). This made validation awkward: you had to pass unchecked values to the parent or extract logic into static helper methods. JEP 513 lifts that restriction, allowing statements to execute before the explicit constructor invocation.
public class PositiveRange extends Range {
public PositiveRange(int low, int high) {
// Validation BEFORE calling super — new in Java 25
if (low < 0 || high < 0) {
throw new IllegalArgumentException("Values must be non-negative");
}
if (low > high) {
throw new IllegalArgumentException("low must be <= high");
}
super(low, high);
}
}This pattern keeps constructors readable and avoids the workaround of static factory methods or validation wrappers. The compiler still ensures that instance fields of the current class are not accessed before the superclass constructor completes.
Scoped Values (JEP 506) — Final
After five preview rounds, Scoped Values are now a permanent part of the platform. They offer a modern, safer, and more performant alternative to ThreadLocal for sharing immutable data across method calls within a thread — and they work especially well with virtual threads.
Unlike ThreadLocal, a scoped value is immutable within its bound scope and is automatically cleaned up when the scope ends. There is no risk of memory leaks from forgotten remove()calls.
import java.lang.ScopedValue;
public class RequestHandler {
private static final ScopedValue<String> CURRENT_USER =
ScopedValue.newInstance();
void handleRequest(String username) {
ScopedValue.runWhere(CURRENT_USER, username, () -> {
processOrder();
sendNotification();
});
}
void processOrder() {
// Access the scoped value anywhere in the call chain
String user = CURRENT_USER.get();
System.out.println("Processing order for: " + user);
}
void sendNotification() {
String user = CURRENT_USER.get();
System.out.println("Sending notification to: " + user);
}
}When to use Scoped Values over ThreadLocal: prefer scoped values when the data is conceptually request-scoped (web request context, transaction ID, user identity) and should not be mutated by callees. They are cheaper to create and rebind, making them a natural fit for the high-concurrency model that virtual threads enable.
Structured Concurrency (JEP 505) — Fifth Preview
Structured Concurrency treats a group of related concurrent tasks as a single unit of work. If one subtask fails, all siblings are cancelled automatically — no more leaking threads or half-finished work. The API integrates tightly with virtual threads and scoped values.
import java.util.concurrent.StructuredTaskScope;
import java.util.function.Supplier;
record UserProfile(String name, List<Order> orders) {}
UserProfile fetchUserProfile(long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> name = scope.fork(() -> fetchUserName(userId));
Supplier<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
// Wait for both tasks; propagate the first failure
scope.join().throwIfFailed();
return new UserProfile(name.get(), orders.get());
}
// Both subtasks are guaranteed to be finished here
}The ShutdownOnFailure policy cancels remaining subtasks the moment any one of them throws. An alternative policy, ShutdownOnSuccess, returns the first successful result and cancels the rest — useful for racing redundant requests against multiple backends.
Stable Values (JEP 502) — Preview
Stable Values provide a thread-safe, lazy initialization mechanism that the JVM can treat as a constant after first assignment. This eliminates the classic double-checked locking pattern and unlocks aggressive JIT optimizations.
import java.lang.StableValue;
public class AppConfig {
// Expensive connection — created only on first access
private static final StableValue<DatabaseConnection> DB =
StableValue.of();
public static DatabaseConnection getConnection() {
return DB.orElseSet(() -> {
System.out.println("Creating database connection...");
return new DatabaseConnection("jdbc:postgresql://localhost/mydb");
});
}
}The first thread to call orElseSet runs the supplier; all subsequent calls return the cached value without locking. Because the JVM knows the value will never change after initialization, it can inline and constant-fold just like a static final field initialized at class-load time.
Stable collections are also provided. For example, StableValue.list()creates a lazily-populated list where each element is computed at most once:
List<Logger> loggers = StableValue.list(10, i ->
Logger.getLogger("module-" + i)
);
// Element 3 is created on first access; subsequent reads are instant
Logger log = loggers.get(3);Primitive Types in Patterns (JEP 507) — Third Preview
Pattern matching now supports primitive types in instanceof and switch expressions. Previously, primitives required manual casting and range checks. JEP 507 lets you write cleaner, safer conversions by matching and extracting primitive values directly.
// Type-safe handling based on the actual value range
String describeHttpStatus(int status) {
return switch (status) {
case 200 -> "OK";
case 301 -> "Moved Permanently";
case 404 -> "Not Found";
case int s when s >= 500 && s < 600 -> "Server Error: " + s;
default -> "Unknown: " + status;
};
}// Safely narrow a long to int without manual range checking
Object value = fetchValue();
if (value instanceof int i) {
System.out.println("int value: " + i);
} else if (value instanceof long l) {
System.out.println("long value: " + l);
} else if (value instanceof double d) {
System.out.println("double value: " + d);
}The compiler performs exact and safely-narrowing conversions at the pattern match site, eliminating an entire class of subtle numeric-conversion bugs.
Module Import Declarations (JEP 511) — Preview
Tired of long import lists when using a library? Module Import Declarations let you import all public types exported by a module with a single statement. Your own code does not need to be inside a module to use this feature.
// Instead of dozens of java.util.* imports:
import module java.base;
void main() {
List<String> names = List.of("Alice", "Bob", "Charlie");
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);
names.stream()
.filter(n -> scores.getOrDefault(n, 0) > 90)
.forEach(IO::println);
}A single import module java.base covers collections, streams, I/O, concurrency, and more. This pairs well with Compact Source Files to make small scripts and teaching examples even cleaner.
Performance and Runtime Improvements
Java 25 includes several under-the-hood improvements that benefit applications without any code changes:
Compact Object Headers (JEP 519)
Object headers have been reduced from 96–128 bits to just 64 bits on 64-bit platforms. For applications that allocate millions of small objects — think microservices, data pipelines, or in-memory caches — this translates to significant memory savings and better cache utilization.
Generational Shenandoah (JEP 521)
The Shenandoah garbage collector now supports generational collection, matching ZGC in having both a single-generation and multi-generation mode. Generational mode focuses young-generation collections on short-lived objects, reducing GC overhead and improving throughput for most workloads.
JFR Enhancements (JEP 518, 520, 525)
Java Flight Recorder gains CPU-Time Profiling for more accurate hot-method identification, Cooperative Sampling that reduces profiling overhead in large applications, and Method Timing & Tracing for fine-grained latency analysis of specific methods without third-party agents.
Ahead-of-Time Compilation Improvements
AOT method profiling and streamlined command-line ergonomics make it easier to pre-compile hot paths. Combined with CDS (Class Data Sharing) improvements, application startup time continues to shrink release over release.
Key Derivation Function API (JEP 510)
A new standard API for key derivation functions (KDFs) such as HKDF joins the security provider framework. Applications that derive encryption keys from shared secrets or passwords can now use a portable, reviewed API rather than vendor-specific libraries.
Migration Tips: Moving to Java 25
Upgrading to Java 25 from earlier LTS versions (17 or 21) is straightforward for most projects, but a few areas deserve attention:
32-bit x86 support removed (JEP 503) — if you still deploy to 32-bit Intel systems, you will need to migrate to a 64-bit OS or stay on an older JDK.
Preview features require opt-in — features like Stable Values (JEP 502), Primitive Patterns (JEP 507), and Module Imports (JEP 511) need
--enable-previewat compile and run time.Replace ThreadLocal with Scoped Values where the data is truly request-scoped and immutable. This is especially impactful if you are already using virtual threads.
Test your GC settings — if you were using Shenandoah, try the new generational mode. Benchmark before and after to confirm the improvement in your workload.
Update build tools — ensure Maven, Gradle, and your IDE support Java 25 source level. Most popular libraries (Spring, Quarkus, Micronaut) have Java 25 compatibility releases.
Conclusion
Java 25 is a landmark LTS release that balances developer experience with runtime performance. Compact Source Files and Flexible Constructors lower the bar for newcomers. Scoped Values and Structured Concurrency modernize multi-threaded code for the virtual-thread era. And under the hood, Compact Object Headers and Generational Shenandoah push throughput and memory efficiency forward.
If your team is evaluating the upgrade, the best approach is to start with a non-production service, enable preview features in a test environment, and measure real-world metrics. The combination of cleaner syntax, safer concurrency, and free performance gains makes Java 25 a compelling target for any project that values long-term stability.
Java 25 Feature Summary
| Feature | JEP | Status |
|---|---|---|
| Compact Source Files | 512 | Final |
| Flexible Constructor Bodies | 513 | Final |
| Scoped Values | 506 | Final |
| Structured Concurrency | 505 | Fifth Preview |
| Stable Values | 502 | Preview |
| Primitive Types in Patterns | 507 | Third Preview |
| Module Import Declarations | 511 | Preview |
| Compact Object Headers | 519 | Final |
| Generational Shenandoah | 521 | Final |
| Key Derivation Function API | 510 | Final |
| Vector API | 508 | Tenth Incubator |
Java 25 proves that the platform continues to evolve in meaningful ways — making code simpler to write, safer to run concurrently, and faster to execute. With LTS backing, it is the release to bet on for production systems in 2025 and beyond.