Java 25 vs Java 26: What’s New, Key Differences, and Which One to Use
-
Last Updated: June 25, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
A Java 25 vs Java 26 comparison guide for developers. Learn what went final in Java 25 LTS, what Java 26 adds (HTTP/3, AOT object caching), how previews progressed, and which version fits your project.
If you have been following the Java release train, you already know it moves fast. A new feature release lands every six months, and keeping track of what actually changed can feel like a full-time job. This is exactly where a clear comparison between Java 25 and Java 26 helps. These two releases sit right next to each other, yet they play very different roles in your day-to-day work.
Java 25 is a Long-Term Support (LTS) release. It is the version most teams will standardize on for years. Java 26, on the other hand, is a regular six-month feature release that indicates where the platform is headed next. So the real question is not just “what is new” but “which one should I actually build on right now”. This article answers both.
We will walk through the backstory, the headline features of each release, the preview features that matured from one version to the next, and a side-by-side breakdown. By the end, you will know exactly how Java 25 and Java 26 differ and which one fits your project. Let’s dig in.
Since Java 10, Oracle and the OpenJDK community ship a feature release every six months. Most of these are short-lived. They receive updates only until the next release arrives. Every few years, though, one release is marked as LTS. That version receives years of security and performance updates, making it safe for production systems that cannot constantly upgrade.
Java 25 reached General Availability on September 16, 2025, and it is an LTS release – the first LTS after Java 21. Java 26 is scheduled for General Availability on March 17, 2026, and it is a non-LTS feature release. That single fact shapes everything else in this comparison.
Why does this matter so much? Because the choice between the two is rarely about which has more features. It is about support and stability. Enterprises building banking systems, payment platforms, or large microservice fleets usually pin to an LTS version. Early adopters and library authors often track the latest feature release to stay ahead. Keep this lens in mind as we go through the features.
Java 25 is special because so many long-running preview features finally became permanent, stable, production-ready APIs. When a feature goes “Final”, you can use it without preview flags and rely on it not changing. For an LTS release, that stability is the whole point.
Let’s look at the most important finalized features, especially the ones that change how you write everyday Java. If you want the full breakdown of everything Java 25 introduced, the dedicated Java 25 articles on javahandson.com (including the popular Java 25 vs Java 21 comparison) cover each feature in depth. This section focuses on what matters most for comparing against Java 26.
This is the feature beginners will feel first. You can now write a source file without a class declaration and use an instance main method instead of the old static one. You then run it directly with a single command. These files also automatically import common classes from java.base module.
// Java 25 - no class, no static, no String[] args needed
void main() {
IO.println("Hello from Java 25!");
}
// Run it directly:
// $ java Hello.java
Notice how much boilerplate disappears. There is no public class, no public static void main(String[] args), and no manual import for printing. This makes Java far more approachable for scripts, demos, small utilities, and teaching – without changing how large applications are written.
If you want to learn more about Compact Source Files and Instance Main Methods JEP 512, then you can refer to this article – https://javahandson.com/jep-512-explained-compact-source-files-and-simpler-java-programs/
Before Java 25, a constructor had to call super(…) or this(…) as its very first statement. That rule often forced awkward workarounds when you wanted to validate arguments early. Java 25 relaxes this. You can now run validation or set up logic before calling the superclass constructor.
class Account {
Account(long balance) {
if (balance < 0) {
// validation BEFORE super() - now allowed
throw new IllegalArgumentException("Balance cannot be negative");
}
super();
}
}
This is cleaner and safer. You can reject bad input before any parent initialization runs, which helps centralize argument checks and avoid partially constructed objects.
If you want to learn more about Flexible Constructor Bodies JEP 513, then you can refer to this article – https://javahandson.com/jep-513-java-25-flexible-constructor-bodies-explained/
Java 25 lets you import all packages exported by a module in a single line using the import module syntax. Importantly, your own code does not need to be a module to use it. This reduces long import lists and simplifies the reuse of modular libraries.
// Import everything java.base exports in one line
import module java.base;
void main() {
var list = List.of(1, 2, 3); // no explicit java.util import
IO.println(list);
}
If you want to learn more about Module Import Declarations JEP 511, then you can refer to this article – https://javahandson.com/module-import-declarations-java-25/
Scoped Values allow a method to share immutable data with the methods it calls on the same thread and with child threads. They are easier to reason about than the older ThreadLocal approach, and they pair naturally with virtual threads. After five previews, this API is now final in Java 25.
If you want to learn more about Scoped Values JEP 506, then you can refer to this article – https://javahandson.com/scoped-values-in-java-25/
Pattern matching has been one of Java’s biggest modern themes, and it keeps getting stronger. By Java 25, you can write clear, type-driven switch logic that replaces long chains of if-else and casts. Here is the style of code this enables:
static String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s.toUpperCase();
case null -> "It was null";
default -> "Unknown type";
};
}
The extension of pattern matching to primitive types is still a preview feature in both Java 25 and Java 26 (it advances from a third to a fourth preview). That progression is a good example of how a single capability matures gradually across releases rather than landing all at once.
If you want to learn more about the primitive pattern, then you can refer to this article – https://javahandson.com/primitive-patterns-in-java-25/
Java 25 also ships several runtime improvements that matter for cloud and container workloads:
Together, these make Java 25 a strong, efficient base for microservices and AI/ML workloads, which is exactly what you want from an LTS release.
Java 26 is a non-LTS feature release scheduled for March 17, 2026. Its feature set is already frozen, which means no new JEPs will be added before it ships – only bug fixes and stabilization work remain. Compared to Java 25, it has fewer brand-new finalized language features and leans more toward runtime additions and the continued maturation of preview APIs.
That focus is not a weakness. It tells you something about the rhythm of the Java release train. An LTS release like Java 25 is where the platform consolidates and makes things permanent. The following feature release tends to introduce fewer forward-looking capabilities and quietly improve runtime performance for your applications. Java 26 fits that pattern almost perfectly.
If you group the Java 26 changes, they fall into three buckets. First, genuinely new capabilities you can reach for in code, led by HTTP/3 in the HTTP Client. Second, runtime and performance work that benefits you automatically – faster startup through ahead-of-time object caching, and better G1 garbage collector throughput. Third, a long-overdue cleanup that removes dead corners of the platform. Let’s go through each in turn.
This is the marquee feature of Java 26. To understand why it matters, a little history helps. Back in JDK 11, JEP 321 gave Java a modern HTTP Client API in the java.net.http package. That client supported HTTP/1.1 and HTTP/2, and it was deliberately designed so that future protocol versions could be added with minimal change. Java 26 cashes in on that design by adding HTTP/3.
So what is HTTP/3? It is the successor to HTTP/2, standardized by the IETF in 2022. The big difference is underneath: instead of running over TCP, HTTP/3 runs over a transport protocol called QUIC, which is secured with TLS 1.3. The practical payoff is more reliable transport, especially on networks with high packet loss. With HTTP/2 over TCP, one lost packet can stall every stream sharing that connection (a problem called head-of-line blocking). QUIC handles this far more gracefully. HTTP/3 is already supported by most browsers and deployed on roughly a third of all websites, so client-side support in Java was overdue.
The single most important thing to understand: HTTP/3 is opt-in. It does not become the default. The client still prefers HTTP/2 by default and transparently falls back to HTTP/1.1 when needed. You explicitly choose HTTP/3 when you want it. A goal of the JEP is that adopting it requires only minor changes to your code. The familiar request pattern still looks like this:
import java.net.http.*;
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://openjdk.org/"))
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
To use HTTP/3, you opt in by setting the preferred version, for example, via HttpClient.Version.HTTP_3 (a new enum value added for this feature) on either the client or the individual request. Treat the exact builder method names as something to confirm against the final Java 26 javadoc, since this shipped as a fresh feature.
A genuinely useful detail is how the client decides whether HTTP/3 can actually be used for a given server, because a server might not support it. The JEP describes a few discovery strategies:
a. Try HTTP/3 first, then fall back. Send the first request over HTTP/3; if a connection can’t be established in a reasonable time, fall back to HTTP/2 or HTTP/1.1. This happens when the request prefers HTTP_3.
b. Race the connections. Attempt an HTTP/3 connection and an HTTP/2 or HTTP/1.1 connection in parallel, and use whichever succeeds first. This happens when the client prefers HTTP_3 But the request doesn’t specify a version.
c. Use Alternative Services. Send the first request over HTTP/2 or HTTP/1.1, and if the server’s response advertises HTTP/3 as available, use HTTP/3 for all subsequent requests.
A few honest scope limits worth knowing, straight from the JEP. This first implementation only works with the default SunJSSE secure socket provider, not with third-party ones. There is no QUIC API for general use, only HTTP/3 built on top of it. And it is client-side only — JEP 517 does not add a server-side HTTP/3 implementation, nor does it touch the old java.net.URL handler, which remains HTTP/1.1.
Why this matters in practice: for chatty microservices, mobile clients on flaky networks, and high-throughput APIs, HTTP/3’s QUIC transport means faster connection setup and smoother behavior under packet loss — and you get it through the same HttpClient you already know, by opting in rather than rewriting your networking layer.
To understand this feature, you first need to understand the problem it solves. Java applications are often scaled by spinning up new JVM instances to handle more load. But a freshly started JVM is slow at first: requests hitting a brand-new instance take noticeably longer than requests hitting a warmed-up one, because the JVM still has to load and link classes and warm up before it runs at full speed. That slow-start penalty shows up as tail latency.
To attack this, JDK 24 introduced an ahead-of-time (AOT) cache via JEP 483 (Ahead-of-Time Class Loading & Linking). The idea is to do expensive class loading and linking once during a separate “training run,” store the result in a cache, and then reuse it in production so those classes appear ready immediately. The numbers are real: the JEP cites the Spring PetClinic demo starting about 41% faster in production because roughly 21,000 classes are already loaded and linked. Later work builds on this same cache — AOT method profiling arrived in JDK 25, and AOT code compilation is planned.
Here’s where Java 26 comes in. The AOT cache stores not just classes but also the Java objects they reference, such as strings and byte arrays. Until now, those cached objects were stored in a GC-specific format — laid out to be bitwise-compatible with how a particular garbage collector arranges objects in the heap, so the JVM could map them straight into memory. The catch: that format was incompatible with ZGC, the low-latency collector. So you were forced into an unpleasant either/or. If you used ZGC to cut GC pause times, you couldn’t use AOT object caching to cut startup time, and vice versa.
JEP 516 removes that forced choice. It adds an option to cache Java objects in a neutral, GC-agnostic format that works with every HotSpot collector, including ZGC, regardless of which GC was used during training or in production. The practical result: you keep the collector that fits your workload and still get the AOT startup benefits. You no longer have to trade one against the other.
How does it work, at a high level? Instead of storing direct memory addresses (which differ per GC), the cache stores object references as logical indices. Reading the cache then means converting those indices back into real references. The JVM does this by streaming the cached objects into memory: when the cache opens, a background thread starts materializing objects one at a time — allocating heap memory, filling in each object’s fields, and reconnecting references by looking them up in a side table. When your application first uses a given class, it synchronizes with the corresponding background thread to ensure the relevant objects are ready. A nice way to think about it, borrowed from the implementation discussion: archived objects end up looking like any other normal object to the GC, which decouples the garbage collector from the caching mechanism.
Why this matters in practice: if you run latency-sensitive services on ZGC — common for large-heap, low-pause workloads — Java 26 lets you layer fast startup on top without switching collectors. For microservices and serverless functions that scale by launching new JVMs, cutting both cold-start time and GC pauses at once is exactly the kind of win that compounds across a fleet.
G1, the Garbage-First collector, is the default garbage collector in the HotSpot JVM, so anything that makes it faster touches a huge number of real applications. G1 is designed to balance latency and throughput: compared to the Parallel and Serial collectors, it does more of its work concurrently with your application, which shortens GC pauses and improves latency. The trade-off is that this balancing act can reduce throughput compared to throughput-oriented collectors. Java 26 narrows that gap.
To understand how, you need one concept: the card table. When G1 reclaims memory, it copies live objects into new regions, so references to those objects (stored in fields of other objects in other regions) must be updated to point to their new locations. Scanning the entire heap to find those references would be far too slow. So G1 maintains a compact data structure called the card table that tracks where cross-region references reside, and updates it whenever a reference is stored in a field.
Those updates are done by tiny snippets of code called write barriers, which the JIT injects into your application wherever it writes object references. And here is the crux of the problem: G1’s write barrier is expensive. When allocating heavily, G1 also runs background “refinement” (optimizer) threads to prevent the card table from growing too large, and those threads must synchronize with your application threads to avoid conflicting updates. That synchronization is exactly what makes the barrier code complex and slow.
The numbers from the implementation discussion make this vivid: G1’s existing write barrier takes on the order of 40–50 machine instructions per single reference store, versus just three or four for the Parallel and Serial collectors. That barrier alone has been shown in research to reduce throughput by roughly 10–20%, and its sheer size also blocks compiler optimizations such as loop unrolling and inlining.
JEP 522’s fix is elegant: introduce a second card table. With two card tables, the application threads and the background refinement threads can each work on their own table without the fine-grained, instruction-by-instruction synchronization they needed before. Coordination instead happens coarsely, by atomically switching between the tables, using generic thread-local handshakes (a mechanism from JEP 312) rather than fragile OS-specific tricks. This lets G1 slim its write barrier down to something much closer to Parallel GC’s, shrinking the injected code and cutting synchronization overhead.
A few things worth being precise about, straight from the JEP’s stated goals. The aim is to reduce G1’s synchronization overhead and the size of its write-barrier code, while keeping G1’s overall architecture intact — there are no changes to how you interact with the collector. It is explicitly not a goal to make G1 match the raw throughput of Parallel or Serial GC; the point is to close the gap, not erase it.
Why this matters in practice: because G1 is the default collector, this is a free win. There’s no new flag and no code change — you run on Java 26 with G1 and get better throughput, with the gains most noticeable in allocation-heavy services that store lots of object references. Interestingly, the JEP notes this approach improves both throughput and latency, since less synchronization helps on both fronts. If garbage collection internals are unfamiliar territory, this topic rewards a dedicated read, as write barriers and card tables come up frequently in senior-level interviews.
Every healthy platform needs to prune dead wood and tighten old guarantees. Java 26 includes two cleanup JEPs worth understanding.
JEP 500 – Prepare to Make Final Mean Final
In Java, marking a field final is supposed to mean its value can never change after it’s set. But there has always been a loophole: using deep reflection (Field.setAccessible(true) followed by Field.set(...)), code could quietly change a final field anyway.
class Person {
private final String name = "Alice"; // should never change...
}
// ...but reflection has historically been able to change it:
Field f = Person.class.getDeclaredField("name");
f.setAccessible(true);
f.set(personInstance, "Bob"); // mutating a 'final' field
This breaks the promise that final is meant to give you. It also blocks performance optimizations: when the JVM can trust that a final field never changes, it can do tricks like constant folding (computing a value once instead of every time). The loophole takes that trust away.
Here’s the key point, and the word “Prepare” in the title is doing a lot of work: Java 26 does not block this yet. It only starts issuing a warning when your code mutates a final field through reflection. The message looks roughly like this:
WARNING: final field name in class Person has been mutated reflectively by class Application.
A future release will turn that warning into a hard error (an IllegalAccessException) by default. Java 26 gives you a control to choose the behavior now, via the --illegal-final-field-mutation flag, which supports warn (the default in Java 26), allow (permit silently), and deny (fail immediately).
Why the slow rollout? Because many legitimate libraries — serialization, dependency injection, and mocking frameworks — have relied on this trick for years. The gradual path gives maintainers time to fix their code before it breaks. This is part of a broader OpenJDK initiative called Integrity by Default, which has been steadily closing these “reach-around” loopholes one release at a time.
Practical takeaway: run your application on Java 26 and watch for these warnings. To find problems early, you can run with --illegal-final-field-mutation=deny, which makes the offending code fail immediately so you can see exactly where it happens. If a third-party library triggers it, that’s your signal to check for an update.
JEP 504 – Remove the Applet API
Applets were tiny Java programs that ran inside a web browser, and back in 1996, they were one of the main reasons people used Java at all. That era is long over. Browsers dropped support for the plugin technology that ran applets years ago, so the java.applet API had become code that nobody could actually use.
One subtle but important detail: the standalone JDK itself never actually ran applets — that was always the job of the now-defunct browser plugin or the old appletviewer tool (removed back in JDK 11). The final piece fell into place when JDK 24 permanently disabled the Security Manager, which was the sandbox applets needed to run untrusted code safely. With that gone, there was simply no path left to run an applet.
So Java 26 removes the entire java.applet package, along with related pieces like the javax.swing.JApplet class. This isn’t a hasty decision — the removal was a roughly ten-year process that began with deprecation in JDK 9 (2017) and deprecation-for-removal in JDK 17 (2021).
For almost every modern codebase, this changes nothing. The rest of the desktop stack — AWT, Swing, and Java 2D — is completely unaffected. The two cases where you’d need to adjust: if you have very old code that still imports java.applet, you’ll need to migrate (the AWT API covers most UI container uses), and if you used java.applet.AudioClip for audio, the replacement is the javax.sound.SoundClip class, which was introduced in JDK 25 as a forward-looking landing spot. If you maintain anything that old, note it before upgrading.
Step back from the individual JEPs, and a clear theme emerges. Java 26 is not chasing flashy syntax. Instead, it is investing in three things that matter for modern, cloud-native Java:
all while steadily cleaning up legacy behavior.
This is the platform optimizing for where Java actually runs today – in containers, microservices, and serverless functions, where startup time, network efficiency, and throughput translate directly into cost and user experience. Combined with the preview features advancing toward final (covered in the next section), Java 26 paints a picture of a platform that is maturing thoughtfully rather than reinventing itself. For a deeper look at how these themes built up over the previous releases, see the related reading linked at the end of this article, including the earlier comparison of Java 25 against Java 21.
A big part of the Java 25 vs Java 26 story is not new features at all – it is preview features maturing by one step. Preview features ship behind a flag so the community can test them before they become final. Tracking how far each one advanced tells you how close it is to being production-ready.
Here is how the major previews moved between the two releases:
| Feature | Java 25 | Java 26 |
| Structured Concurrency | 5th preview (JEP 505) | 6th preview (JEP 525) |
| Primitive Types in Patterns, instanceof, switch | 3rd preview (JEP 507) | 4th preview (JEP 530) |
| PEM Encodings of Cryptographic Objects | 1st preview (JEP 470) | 2nd preview (JEP 524) |
| Stable Values -> Lazy Constants | 1st preview, “Stable Values” (JEP 502) | 2nd preview, renamed “Lazy Constants” (JEP 526) |
| Vector API | 10th incubator (JEP 508) | 11th incubator (JEP 529) |
One detail worth highlighting: the feature previewed as “Stable Values” in Java 25 was renamed to “Lazy Constants” in Java 26. If you read about Stable Values and could not find them, it is because they have been renamed. Same idea, clearer name.
Structured Concurrency is also worth watching. It simplifies handling multiple concurrent tasks as a single unit of work, so that if one subtask fails, the others can be canceled cleanly rather than leaking. Reaching a sixth preview suggests the design is converging. If you write concurrent code with virtual threads, this is the API to keep an eye on. The mental model looks like this:
// Structured Concurrency (preview) - tasks as one unit of work
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> fetchUser(id));
var order = scope.fork(() -> fetchOrder(id));
scope.join(); // wait for both
return new Dashboard(user.get(), order.get());
}
// If either task fails, the scope handles cancellation cleanly.
This pattern makes concurrent code read almost like sequential code, while still running tasks in parallel. Because the exact API has shifted across previews, treat the snippet above as conceptual and verify the current signatures against the Java version you actually target.
Let’s pull everything together into one clear comparison. This is the table to bookmark when someone asks you to summarize the difference quickly.
| Aspect | Java 25 | Java 26 |
| Release type | LTS (Long-Term Support) | Non-LTS feature release |
| GA date | September 16, 2025 | March 17, 2026 (scheduled) |
| Support horizon | Years of updates (OpenJDK EOL around Sep 2030) | Until the next feature release |
| Headline theme | Many features go Final + runtime efficiency | HTTP/3, broader AOT, GC throughput |
| Best for | Production, enterprise, long-lived systems | Early adopters, library authors, staying ahead |
The pattern is clear. Java 25 is about making things permanent and stable. Java 26 is about adding a few new capabilities and pushing previews one step closer to final. Most of the Java 26 “newness” is continued maturation rather than a flood of brand-new language features.
This is the practical heart of the Java 25 vs Java 26 decision. The right answer depends entirely on your context, so here is a simple way to choose.
For most teams reading this, the honest recommendation is to build on Java 25 LTS for real work and keep an eye on Java 26 to understand what is coming. You get stability today and a clear preview of tomorrow.
When developers compare these two releases, a few misunderstandings recur. Avoiding them will save you time.
These two releases are great interview material because they test whether you understand Java’s release model, not just syntax. Here are points worth remembering.
If you can clearly articulate the LTS-versus-feature-release tradeoff with concrete examples, you will stand out. It shows you think about maintenance and risk, not just shiny features.
Before we wrap up, here is the short version you can keep in your head:
One honest caveat. Java 25 is already generally available, so its feature list is final. Java 26, at the time of writing, is scheduled for GA on March 17, 2026, with its feature set frozen but not yet shipped. Frozen means no new JEPs will be added, but minor details of preview-stage APIs may still change before release.
So when you start coding against Java 26 features, always confirm exact API names and behavior against the official Java 26 release notes once it ships. For anything mission-critical, the official OpenJDK and Oracle documentation is the source of truth.
The Java 25 vs Java 26 comparison really comes down to one idea: stability versus direction. Java 25 is the LTS release that turns years of preview features into stable, production-ready tools, backed by long-term support. It is the version most teams should build on today.
Java 26 is the next step on the road. It brings HTTP/3 to the standard HTTP Client, extends ahead-of-time object caching to any garbage collector, improves G1 throughput, and pushes several previews closer to final. It is the release to study if you want to see where Java is going – and to adopt if you are comfortable upgrading often.
Pick Java 25 for serious, long-lived work. Watch Java 26 to stay ahead of the curve. Understand both, and you will always know exactly which JDK belongs in your next project.