Skip to main content

What’s Coming in JDK 28: Value Classes Preview and Strict Field Initialization

JDK 28 will be released in March 2027, and its headline feature is one Java developers have waited a decade for: value classes, the first preview to come out of Project Valhalla. Early-access builds already exist, so instead of summarising a JEP page this article installs one and tries the features. You need to know what a Java record is and that == on two objects normally asks “is this the very same object?”. That question is exactly what value classes change.
Status: early access, snapshot of 28 September 2026. Tested on OpenJDK 28-ea+17 (build of 24 September), compared with JDK 25.0.4.1. Everything here can change before general availability, and this article will be updated as JEP targets change. Nothing below is a recommendation to use preview features in production. Companion code: javademos/jdk28-preview.

What is on the JDK 28 list so far

The JDK 28 release page lists which JEPs are targeted. Because that page could not be fetched by my tooling, the list below is what a published summary reported in August, plus one announcement from 20 September. It is a quotation, not something I ran, and JEP 542 was only “proposed to target” at that time:
# 07-jep-status (quoted from published articles on 2026-09-28; NOT produced by running code; status changes weekly)
InfoQ, "JDK 27 and JDK 28: What We Know So Far" (https://www.infoq.com/news/2026/08/java-27-so-far/), August 2026:
  Targeted to JDK 28: JEP 541 Deprecate the macOS/x64 Port for Removal; JEP 540 Simple JSON API (Incubator);
  JEP 539 Strict Field Initialization in the JVM (Preview); JEP 535 Shenandoah GC: Generational Mode by Default; JEP 401 Value Objects (Preview)
  Proposed to target: JEP 542 PEM Encodings of Cryptographic Objects
  "Scheduled for a GA release in March 2027"
Inside.java, 2026-09-20 (https://inside.java/2026/09/20/jep401-target-jdk28/): "JEP targeted to JDK 28: 401: Value Objects (Preview)"

Captured in 07-jep-status.txt.

Of these, this article runs four: 401 (value objects), 539 (strict field initialization, which shows up as a flag on the class file), 542 (PEM encodings) and 540 (the JSON API). It could not test 535 (Shenandoah), for a reason explained near the end. The build itself reports its own release date:
# 00-environment
--- JDK 28 early-access:
openjdk version "28-ea" 2027-03-23
OpenJDK 64-Bit Server VM (build 28-ea+17-1282, mixed mode, sharing)

Captured in 00-environment.txt.

JEP 401: value classes, the change to what == means

Today every object has an identity: a hidden “this exact instance” that ==, synchronized and System.identityHashCode all rely on. A value class opts out of identity. Two value objects with the same field values are simply the same value, the way two ints of 5 are. The reward is that the JVM is free to store them flat, without a pointer and object header per instance. The syntax is one new modifier. A value record is the smallest example:
    value record Point(int x, int y) {}

    public static void main(String[] args) {
        Point p = new Point(1, 2), q = new Point(1, 2);
        System.out.println("p == q: " + (p == q) + ", equals: " + p.equals(q));
        System.out.println("isValue: " + p.getClass().isValue());
        Object o = p;
        try {
            synchronized (o) { System.out.println("locked"); }
        } catch (Throwable t) {
            System.out.println("synchronized on a value object: " + t);
        }
    }
}

Source: ValueClasses.java, lines 3–17.

p == q: true, equals: true
isValue: true
synchronized on a value object: java.lang.IdentityException: Cannot synchronize on an instance of value class ValueClasses$Point

Captured in 01-value-classes.txt.

What == asksIdentity object (today)a == b asks: same instance?equal fields, different instances: falseValue object (JDK 28 preview)a == b asks: same state?equal fields: trueIdentity gives the object a hidden address; a value has only its fields.That is why locking on a value object cannot work: there is nothing to lock.
The diagram summarises the transcript above it: p == q is true for two separately constructed points, isValue() confirms the class, and locking on a value object throws an IdentityException. Code that synchronises on a boxed number or uses object identity as a map key is what will notice.
The surprise: Integer is a value class too. With preview features enabled, the JDK’s own wrapper classes behave as value classes. The same source line that has always been a classic Java trap now gives the opposite answer:
--- IntegerIdentity.java: same source, three runs
JDK 25:                 Integer 1000 == Integer 1000: false
JDK 28, no preview:     Integer 1000 == Integer 1000: false
JDK 28, --enable-preview: Integer 1000 == Integer 1000: true

Captured in 01-value-classes.txt.

On JDK 25 and on JDK 28 without --enable-preview, two boxed 1000s are different objects and == is false. With the preview on, it is true. Nothing in the source changed. Code that accidentally depended on the old answer — for instance a test asserting that two boxes are not the same — behaves differently under the flag.

Turning it on, and what happens if you forget

Preview features are off by default and are deliberately hard to use by accident. Two separate errors guard the door, one for compiling and one for running:
# 02-forgot-the-preview-flag
--- javac ValueClasses.java (no --enable-preview):
src/ValueClasses.java:3: error: value classes are a preview feature and are disabled by default.
	value record Point(int x, int y) {}
	^
  (use --enable-preview to enable value classes)
--- java ValueClasses (compiled with the flag, run without):
Error: LinkageError occurred while loading main class ValueClasses
	java.lang.UnsupportedClassVersionError: Preview features are not enabled for ValueClasses (class file version 72.65535). Try running with '--enable-preview'

Captured in 02-forgot-the-preview-flag.txt.

The class file compiled with the flag is stamped as a preview class (version 72.65535), and no JVM will load it without the same flag. This is what makes preview safe for libraries: a jar built with preview features cannot silently end up on a production JVM that does not expect it.
Going deeper: what to try, and what to leave alone
Good candidates for an experiment are small immutable data types: coordinates, money amounts, identifiers. Bad candidates are anything that is locked, used as a lock, or mutated. I did not measure memory or speed benefits, which are the reason for the feature; the transcripts above show the language behaviour only. Because the class file format is stamped with a preview version, a jar compiled this way should not be published to a repository other people consume.

JEP 539: strict field initialization, seen from the class file

Value classes have to guarantee something ordinary classes do not: a field is set before anything can look at the object. JEP 539 gives the JVM a field flag for that, so the guarantee is enforced by the JVM rather than by hope. You will not write it by hand; the compiler sets it. The difference is visible with javap by comparing a value class to an identical identity class:
    static value class Money { final long cents; Money(long c) { cents = c; } }
    static class Legacy { final long cents; Legacy(long c) { cents = c; } }
    public static void main(String[] args) { new Money(1); new Legacy(1); }

Source: StrictFieldProbe.java, lines 3–5.

# 03-strict-init-flags (javap -v -p, field flags only)
--- StrictFieldProbe$Money
  final long cents;
    flags: (0x0810) ACC_FINAL, ACC_STRICT_INIT
--- StrictFieldProbe$Legacy
  final long cents;
    flags: (0x0010) ACC_FINAL

Captured in 03-strict-init-flags.txt.

The value class field carries ACC_STRICT_INIT; the identity class does not. I found no source-level way to request the flag on an ordinary class in this build, so I can only say that javac sets it for value classes.

JEP 542: PEM encodings, now without a flag

PEM is the familiar -----BEGIN PUBLIC KEY----- text format for keys and certificates. Java had no standard API for it until PEMEncoder and PEMDecoder arrived as a preview. The same program is compiled and run on both JDKs:
        g.initialize(new ECGenParameterSpec("secp256r1"));
        KeyPair kp = g.generateKeyPair();
        String pem = PEMEncoder.of().encodeToString(kp.getPublic());
        System.out.println(pem.lines().findFirst().get());
        PublicKey back = PEMDecoder.of().decode(pem, PublicKey.class);
        System.out.println("round trip equal: " + back.equals(kp.getPublic()));

Source: PemRoundTrip.java, lines 8–13.

# 04-pem-encodings (JEP 542): PemRoundTrip.java
--- JDK 28, no flags:
-----BEGIN PUBLIC KEY-----
round trip equal: true
--- JDK 25, no flags:
src/PemRoundTrip.java:10: error: PEMEncoder is a preview API and is disabled by default.
		String pem = PEMEncoder.of().encodeToString(kp.getPublic());
		             ^
--- JDK 25, --enable-preview:
-----BEGIN PUBLIC KEY-----
round trip equal: true

Captured in 04-pem-encodings.txt.

On JDK 25 the compiler refuses the API unless preview is enabled. On the 28 build it compiles and runs with no flag, which is consistent with the API being made final, although the status source I could read still called it “proposed”. If the status changes back, this transcript is what would need to be rerun.

JEP 540: a JSON API in the JDK, as an incubator

JEP 540 adds a small JSON parser and tree to the JDK as an incubator module, a stronger warning than preview: the API may change or disappear. It is not on the default module path, so the compiler cannot even see it until you ask:
# 05-json-incubator (JEP 540): JsonDemo.java on JDK 28
--- module and API surface:
jdk.incubator.json@28-ea
exports jdk.incubator.json
requires java.base mandated
  public static jdk.incubator.json.JsonValue parse(java.lang.String);
  public static jdk.incubator.json.JsonValue parse(char[]);
  public static java.lang.String toDisplayString(jdk.incubator.json.JsonValue, java.lang.String);
--- without --add-modules:
src/JsonDemo.java:1: error: package jdk.incubator.json is not visible
import jdk.incubator.json.*;
--- with --add-modules jdk.incubator.json:
name = Ada
second language = fr
age + 1 = 37
missing key -> Optional.empty
{
  "name": "Ada",
  "langs": [
    "en",
    "fr"
  ],
  "age": 36
}

Captured in 05-json-incubator.txt.

Not a Jackson replacement. The API surface I saw is small: Json.parse, a value tree with get, asString, asInt and tryGet, and a pretty printer. There is no data binding to your own classes in this build. I did not benchmark it.

What I could not test: Shenandoah (JEP 535)

# 06-shenandoah (JEP 535: generational mode by default)
--- JDK 28 EA build, -XX:+UseShenandoahGC:
Error occurred during initialization of VM
Option -XX:+UseShenandoahGC not supported
--- JDK 25 Temurin, -XX:+UseShenandoahGC -Xlog:gc*:
[0.007s][info][gc     ] Using Shenandoah
[0.009s][info][gc,init] Mode: Snapshot-At-The-Beginning (SATB)
[0.016s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000000020000000-0x0000000020d96000-0x0000000020d96000), size 14245888, SharedBaseAddress: 0x0000000020000000, ArchiveRelocationMode: 1.

Captured in 06-shenandoah.txt.

The early-access build I downloaded does not include the Shenandoah collector at all, so JEP 535 (generational mode by default) could not be exercised. The transcript also shows what JDK 25’s Shenandoah reports today, as the baseline to compare against when a build with Shenandoah is available.

Should you try JDK 28 now?

Try it on a scratch project, not on your build. It costs one download and a flag, and it is the cheapest way to find out whether your code relies on object identity in places you did not expect (locking on boxes, identity maps, == on wrappers). Do not commit preview class files anywhere. Everything above is early access: rerun the scripts against each new build before trusting a claim.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.