Try Out JEP 401 Value Classes and Objects

The Valhalla team recently published an early-access JDK build that fully implements JEP 401: Value Classes and Objects (Preview). There’s still a lot of work to do to get this code into a future release of the JDK. Until then, now is a great time for anyone who is interested to try out this transformative new feature!

Getting the Early-Access Builds

To get started, go to jdk.java.net/valhalla and download an early-access JDK build. You can review the release notes for a quick summary of what’s included.

Unzip the package, put it somewhere handy, and refer to its bin directory to run commands like java and javac. On my Mac, I’ll set an environment variable for easy access to these commands in the examples below:

% -> export jdk401="$PWD/jdk-26.jdk/Contents/Home/bin"

% -> "$jdk401"/java --version
openjdk 26-jep401ea2 2026-03-17
OpenJDK Runtime Environment (build 26-jep401ea2+1-1)
OpenJDK 64-Bit Server VM (build 26-jep401ea2+1-1, mixed mode, sharing)

Experimenting with Value Objects

As the JEP explains, value objects are instances of value classes, which have only final fields and lack object identity. A handful of JDK classes, including Integer and LocalDate, become value classes when we run Java in preview mode.

In JShell, Objects.hasIdentity makes it easy to tell which objects are value objects and which are regular identity objects:

% -> "$jdk401"/jshell --enable-preview
|  Welcome to JShell -- Version 26-jep401ea2
|  For an introduction type: /help intro

jshell> Objects.hasIdentity(Integer.valueOf(123))
$1 ==> false

jshell> Objects.hasIdentity("abc")
$2 ==> true

jshell> Objects.hasIdentity(LocalDate.now())
$3 ==> false

jshell> Objects.hasIdentity(new ArrayList<>())
$4 ==> true

Value objects behave just like identity objects in most ways. But one difference is that == can’t tell whether two value objects are “the same object” or not-they have no identity to compare. Instead, == tests whether two value objects are statewise-equivalent: instances of the same class with the same field values.

jshell> LocalDate d1 = LocalDate.now()
d1 ==> 2025-10-23

jshell> LocalDate d2 = d1.plusDays(365)
d2 ==> 2026-10-23

jshell> LocalDate d3 = d2.minusDays(365)
d3 ==> 2025-10-23

jshell> d1 == d3
$8 ==> true

Statewise equivalence is no substitute for a meaningful equals method designed by a class author. In some cases, two instances of a value class with different states should still be considered equal. So the best practice, as usual, is to avoid the == operator and prefer equals for comparisons.

You can declare your own value classes with the value keyword. Many record declarations are good candidates to be value classes:

jshell> value record Point(int x, int y) {}
|  created record Point

jshell> Point p = new Point(17, 3)
p ==> Point[x=17, y=3]

jshell> Objects.hasIdentity(p)
$11 ==> false

jshell> new Point(17, 3) == p
$12 ==> true

Value Object Performance

Why bother to declare a value class instead of regular identity class?

One reason is a semantic one: If your class represents immutable domain values that are interchangeable when they have the same state, giving these objects all the features of identity just adds unnecessary complexity. Better to declare a value class and give up identity entirely.

But the most compelling reason is that the JVM can optimize value objects in ways that are impossible for regular objects. For example, a reference to a value object doesn’t have to point to a canonical memory location for that object. Instead, the state of the object can be embedded in the reference itself. This technique is called heap flattening, and can make a huge difference in the cost of loading objects from memory.

As a test, let’s create a very large array of LocalDate value objects and add up all of their year values. To simulate a realistic distribution of objects in memory, we’ll populate the array from an unsorted HashSet of LocalDate objects. We can do some rudimentary profiling by tracking the wall-clock time required to iterate through the array. (Note: For more accurate profiling, JMH should be used.)

void main(String... args) {
    int size = 50_000_000;
    if (args.length > 0) size = Integer.parseInt(args[0]);
    LocalDate[] arr = makeArray(size);
    for (int i = 1; i <= 5; i++) {
        double t = time(() -> sumYears(arr));
        IO.println("Attempt " + i + ": " + t);
    }
}

/// Expensive task to be timed
long sumYears(LocalDate[] dates) {
    long result = 0;
    for (var d : dates) result += d.getYear();
    return result;
}

/// Make an array of LocalDates, unpredictably ordered
LocalDate[] makeArray(int size) {
    HashSet<LocalDate> set = new HashSet<>();
    for (int i = 0; i < size; i++) set.add(LocalDate.ofEpochDay(i));
    return set.toArray(new LocalDate[0]);
}

/// Run a task and report the elapsed wall-clock time in ms
double time(Runnable r) {
    var start = Instant.now();
    r.run();
    var end = Instant.now();
    return Duration.between(start, end).toNanos() / 1_000_000.0;
}

As a baseline, when I put this code in a DateTest.java file and run it on my MacBook Pro without preview features enabled, I get the following:

% -> "$jdk401"/java DateTest.java
Attempt 1: 82.703
Attempt 2: 77.716
Attempt 3: 74.959
Attempt 4: 71.962
Attempt 5: 71.915

When I turn on preview features, LocalDate becomes a value class, and its instances can be flattened directly in the array. By avoiding extra memory loads, the JVM can achieve a nearly 3x speedup!:

% -> "$jdk401"/java --enable-preview DateTest.java
Attempt 1: 41.959
Attempt 2: 38.992
Attempt 3: 25.466
Attempt 4: 28.404
Attempt 5: 25.027

Results will vary on different machines and different array sizes. But the point is that by using value objects in our performance-critical computation, we’ve enabled the JVM to make significant new optimizations that are impossible for identity objects.

Next Steps

This is beta software, and it’s sure to have some bugs and surprising performance pitfalls. Now is a great time for interested users to download the early-access build and try it out on their performance-sensitive workloads. Feedback at valhalla-dev@openjdk.org is welcome and encouraged!

Of course, sprinkling the value keyword around a code base is not going to automatically address whatever performance bottlenecks the program faces. Users are encouraged to review JEP 401 to get a better sense of what kind of optimizations are possible, and use profiling tools like JDK Flight Recorder to see how value objects affect their program’s performance.