Java 26 and 27: What Is Next
Understand where Java 26 and 27 sit in the release cadence, what each changed for back-end services, and how to keep a Java 25 codebase ready.
Runtime.version().feature() // 25 on this course's JDK
Runtime.Version.parse("26.0.1").compareTo(Runtime.Version.parse("25.0.4"))Java ships a feature release every six months, in March and September, and every two years one of them is a long-term support (LTS) release. Java 25 (September 2025) is the current LTS. Java 26 followed in March 2026 and Java 27 in September 2026; both are non-LTS releases that receive updates only until the next release arrives. On the two-year cadence the next LTS is planned to be Java 29, in September 2027. This course targets 25.
Java 26 changes worth knowing
- HTTP/3 for the HTTP Client (JEP 517).
HttpClient.Version.HTTP_3is opt-in; the default stays HTTP/2. - Prepare to make final mean final (JEP 500). Changing a
finalfield through deep reflection (Field.setAccessible(true)thenset) now produces a warning, as a step towards denying it by default. Serialization and dependency-injection libraries that do this will need updates. - The Applet API is removed (JEP 504).
- Ahead-of-time object caching with any garbage collector (JEP 516), so the AOT cache from Java 24 and 25 also works with ZGC.
- Previews continued: structured concurrency (sixth preview, JEP 525), lazy constants, the renamed stable values (second preview, JEP 526), and primitive types in patterns (fourth preview, JEP 530).
Java 27
Java 27 reached general availability on 15 September 2026. It is not an LTS release: it gets updates only until Java 28 ships in March 2027. It has nine JEPs; four are final features that matter to back-end services:
- G1 is the default collector in all environments (JEP 523). Before this, the JVM picked the Serial collector on small machines, which caught out services in containers with one CPU or little memory. If you rely on that choice, set
-XX:+UseSerialGCexplicitly. - Post-quantum hybrid key exchange for TLS 1.3 (JEP 527). When both sides support it, the handshake combines a classical and a post-quantum key exchange, which protects traffic recorded today against future quantum attacks without code changes in the service.
- Compact object headers on by default (JEP 534). The option you could switch on in 25 is now the default, so services with many small objects use less heap. Re-check memory limits and GC tuning after the upgrade.
- JFR data redaction (JEP 536), so Flight Recorder recordings taken in production can leave out sensitive values before they are shared.
Lazy constants are still a preview in 27 (JEP 531), so the API can change again.
How to stay ready on 25
- Run CI on the newest release as well as 25. A non-blocking job on the latest JDK shows new warnings and removed APIs months before the next LTS.
- Treat new warnings as work items. The platform is moving towards "integrity by default": warnings about JNI,
sun.misc.Unsafeand now final field mutation come first, errors later. - Avoid shipping preview features. Preview APIs can and do change between 25 and 26; code that uses them must be recompiled for each release.
- Detect features at run time instead of parsing version strings by hand:
Runtime.version().feature()or a lookup of an enum constant tells you what the JVM supports.
Mistakes people make
- Moving production to every non-LTS release without a plan to follow it six months later; when the next release ships, the old one stops getting updates.
- Reading a preview JEP and assuming the final API will look the same.
- Comparing versions as strings, where "9" sorts after "25".
Example
When to use it
- Adding a non-blocking CI job on the newest JDK so warnings such as final field mutation show up early.
- Rolling out HTTP/3 to an outbound HTTP client only on hosts that run Java 26 or later, with HTTP/2 elsewhere.
- Writing an upgrade policy that keeps production on LTS releases and tests each non-LTS release as it ships.
More examples
Comparing versions properly
<code>Runtime.Version.parse</code> understands the version format and compares feature, interim and update numbers as numbers. String sorting puts 9 after 27, a classic bug in deployment scripts.
import java.util.Comparator;
import java.util.List;
void main() {
List<String> seen = List.of("25.0.4", "9.0.4", "26.0.1", "21.0.8", "27");
List<String> asStrings = seen.stream().sorted().toList();
List<String> asVersions = seen.stream()
.map(Runtime.Version::parse)
.sorted(Comparator.naturalOrder())
.map(Runtime.Version::toString)
.toList();
IO.println("string sort: " + asStrings);
IO.println("version sort: " + asVersions);
Runtime.Version minimum = Runtime.Version.parse("25");
for (String s : seen) {
boolean ok = Runtime.Version.parse(s).compareTo(minimum) >= 0;
IO.println(s + (ok ? " supported" : " too old"));
}
}
// Output:
// string sort: [21.0.8, 25.0.4, 26.0.1, 27, 9.0.4]
// version sort: [9.0.4, 21.0.8, 25.0.4, 26.0.1, 27]
// 25.0.4 supported
// 9.0.4 too old
// 26.0.1 supported
// 21.0.8 too old
// 27 supportedFinal field mutation that Java 26 warns about
Some libraries and tests rewrite <code>final</code> fields this way. JEP 500 makes Java 26 warn about it, as a first step towards blocking it, so replacing the pattern now avoids a forced change later.
import java.lang.reflect.Field;
class Config {
private final String region;
Config(String region) { this.region = region; }
String region() { return region; }
}
void main() throws Exception {
Config config = new Config("eu-west-1");
// Deep reflection rewriting a final field: silent on 25, a warning on 26.
Field f = Config.class.getDeclaredField("region");
f.setAccessible(true);
f.set(config, "us-east-1");
IO.println("after reflection: " + config.region());
// The future-proof way: build a new object instead of mutating the old one.
Config fixed = new Config("eu-central-1");
IO.println("new object: " + fixed.region());
}
// Output:
// after reflection: us-east-1
// new object: eu-central-1Choosing a code path by feature release
Keeping the decision in one function makes it easy to test every branch without installing every JDK. In real code, prefer checking for the capability itself where you can, as the main example does with the enum constant.
record Plan(String name, String detail) {}
static Plan planFor(int feature) {
if (feature >= 26) return new Plan("modern", "try HTTP/3, keep HTTP/2 fallback");
if (feature >= 25) return new Plan("lts", "HTTP/2, virtual threads, scoped values");
return new Plan("unsupported", "upgrade to Java 25");
}
void main() {
for (int f : new int[] {21, 25, 26, 27}) {
Plan p = planFor(f);
IO.println("Java " + f + ": " + p.name() + " (" + p.detail() + ")");
}
IO.println("this JVM: " + planFor(Runtime.version().feature()).name());
}
// Output:
// Java 21: unsupported (upgrade to Java 25)
// Java 25: lts (HTTP/2, virtual threads, scoped values)
// Java 26: modern (try HTTP/3, keep HTTP/2 fallback)
// Java 27: modern (try HTTP/3, keep HTTP/2 fallback)
// this JVM: lts
Discussion