int a = 20260819;
int b = 20260819;
var x = LocalDate.of(2026, 8, 19);
var y = LocalDate.of(2026, 8, 19);You’ve probably heard a lot about Project Valhalla:
indirection and flattening
object headers and density
identity and custom primitives
universal and specialized generics
performance benefits
Forget all that for now!
We will start where Java stands today.
Then we discuss:
JDK value classes
custom value classes
restrictions and behavior
optimization opportunities
All that is available in JDK 28:
But only with --enable-preview.
(Otherwise, even JDK value classes are regular classes.)
All quotes come from JEP 401: Value Objects (Preview).
A must-read before you start using value classes!
At the end, we can discuss
upcoming features from Project Valhalla.
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
Which of these variable pairs can be distinguished?
int a = 20260819;
int b = 20260819;
var x = LocalDate.of(2026, 8, 19);
var y = LocalDate.of(2026, 8, 19);a and b can’t
x and y can
⇝ How?
LocalDate (and all other reference types) have identity,
a way to distinguish otherwise identical objects.
var x = LocalDate.of(2026, 8, 19);
var y = LocalDate.of(2026, 8, 19);
var identical = x == y; // false
synchronized (x) {
// x is "locked", y isn't
}For mutable objects, identity is important: It lets us distinguish two objects that have the same state now but may have different states in the future.
var list1 = new ArrayList<String>();
var list2 = new ArrayList<String>();
var equal = list1.equals(list2); // true
var identical = list1 == list2; // false⇝ Mutable objects can be equal but never interchangeable.
But equal immutable objects are usually interchangeable.
var x = LocalDate.of(2026, 8, 19);
var y = LocalDate.of(2026, 8, 19);Does it make sense to distinguish interchangeable objects?
Integer x1 = new Integer(0);
Integer y1 = new Integer(0);
var identical1 = x1 == y1; // ?Integer x2 = 0;
Integer y2 = 0;
var identical2 = x2 == y2; // ?Integer x3 = 2026;
Integer y3 = 2026;
var identical3 = x3 == y3; // ?var x1 = new String("0");
var y1 = new String("0");
var identical1 = x1 == y1; // ?var x2 = "0";
var y2 = "0";
var identical2 = x2 == y2; // ?var x3 = "0";
var y3 = "" + RandomGenerator.getDefault().nextInt(1);
var identical3 = x3 == y3; // ?Tracking identity is expensive.
Escape analysis exists to avoid it.
Current state:
all reference types have identity
identity distinguishes objects
many equal immutable objects are interchangeable
for many immutable classes:
identity is meaningless
identity can cause confusion
identity comes with a run-time cost
Wouldn’t it be nice to be able to opt out of identity?
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
Enter JEP 401 in JDK 28 with --enable-preview:
We introduce value objects to model simple immutable data. A value object is an instance of a value class, declared with the
valuemodifier. Classes without the value modifier are identity classes, and their instances are identity objects.
java.lang : Integer, Long, Float, Double, Byte, Short, Character, Boolean, Number, Record
java.util : Optional, OptionalInt, OptionalLong, OptionalDouble
java.time : LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetTime, OffsetDateTime, Duration, Instant, Period, Year, YearMonth, MonthDay
java.time.chrono : MinguoDate, HijrahDate, JapaneseDate, ThaiBuddhistDate
var x = LocalDate.of(2026, 8, 19);
var y = LocalDate.of(2026, 8, 19);
var identical = x == y; // true
// synchronized (x) {
// ⇝ compile error:
// required: a type with identity
// }Integer x1 = new Integer(0);
Integer y1 = new Integer(0);
var identical1 = x1 == y1; // true
Integer x2 = 0;
Integer y2 = 0;
var identical2 = x2 == y2; // true
Integer x3 = 2026;
Integer y3 = 2026;
var identical3 = x3 == y3; // trueThe
Stringclass, due to some dependencies on object identity in its API and implementation, is not a value class, so instances ofStringare always identity objects.
Project Valhalla is exploring the option
to migrate more JDK classes to value classes:
It’s worth asking which other JDK API classes might want to become value classes someday. I explored the question in conjunction with the CSR (JDK-8339199), and want to memorialize the experience here, for future reference.
– Dan Smith, Identifying JDK value class candidates
You can declare your own value classes by applying the
valuemodifier to any class whose instances should be immutable and interchangeable:Immutable — All instance fields of the class are final, and the value represented by an instance will not change over time; and
Interchangeable — It is not necessary to distinguish between two separately-created instances that represent the same value.
value class ComplexNumber {
// ...
}
value record Point(int x, int y) {
}(Fun fact: The only new syntax is value.)
In most respects, value objects work the way that objects have always worked in the language: They have fields and methods, they are handled by reference, and their references can be
null.
Value classes are conceptually simple:
value classes:
are reference types
opt out of identity
are immutable and interchangeable (!)
30 JDK value classes
custom value classes with value
But there are a few implications.
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
Value classes are immutable and interchangeable,
which restricts some class-building capabilities.
Value classes are immutable,
so all fields are implicitly final.
(Truly final - even to reflection.)
Limited superclasses:
A value class can extend either
java.lang.Objector an abstract value class, but not an identity class.
No subclasses:
The class itself is also implicitly final, so it cannot be extended.
Java 25 introduced the early construction phase:
class ComplexNumber extends Number {
ComplexNumber(double r, double i) {
// early construction
this.r = r;
this.i = i;
super();
// late construction
LOGGER.log("Created: " + this);
}
}By default, code runs during late construction.
During early construction:
can write and read fields
no other access to this
(e.g. no instance method calls)
⇝ Object can be initialized before other code "sees" it.
For value classes:
by default, code runs during early construction
call to super(…) or this(…):
requires that all fields are set
is needed to enter late construction
⇝ Initial mutation of final fields can never be observed.
Except for the aforementioned restrictions,
inheritance works as for identity classes:
have Object at their inheritance hierarchy root
can implement interfaces
can be abstract
(interpret as "abstract class is value-ready")
Everything else works as for identity classes, too:
inherit equals, hashCode, toString from Object
no restriction on the types of fields
(particularly both value abd identity classes work)
Declaration site restrictions:
fields and class are final
can’t extends identity classes
(except java.lang.Object)
by default, construction code runs
during early construction
Everything else works as usual for declaration.
But there are behavioral differences.
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
Value classes lack identity,
which changes/prevents identity-based operations.
For identity classes, new guarantees an instance
that’s not == to any other.
For a value class, invoking the
newoperator might not allocate a fresh object.
⇝ Conceptually, every constructor could act
like a caching factory method.
Value class fields are truly immutable.
Using deep reflection, as embodied in the
setAccessibleandsetmethods of thejava.lang.reflect.FieldAPI, to mutate the fields of a value object is not supported. […] Libraries must initialize instances of a value class using the class’s constructors.
Historically, == compares identity.
After the introduction of value objects, the purpose of the
==operator is to test whether two referenced objects are indistinguishable.
For identity objects:
indistinguishable and identical are the same
the evolving definition has the same semantic
The
==operator compares value objects by comparing the objects' field values, so references to two objects are==if the objects have identical field values.
That means:
for primitive fields: bitwise equality
for reference type fields: indistinguishability
⇝ For value class fields, == is recursive.
Operations like wait, notify, and synchronize
interact with an object’s monitor.
Object monitors are tied to an object’s identity.
⇝ These operations do not work on value objects.
(Compile error if possible, otherwise run-time exception.)
The
finalizemethod of a value object is never invoked by the garbage collector. javac issues identity warnings for value classes that override finalize.
(Finalization is deprecated for removal - JEP 421 ⑱)
Serialization of record instances
works through constructor invocation.
Serialization of value records works automatically
Otherwise, serialization creates instances with all-null fields
and assigns them later, but value objects prohibit this.
Serialization of non-record value classes requires manual intervention. Value classes that implement
Serializablemust implement thewriteReplaceandreadResolvemethods so that a replacement object is serialized and deserialized in place of the value object itself.
⇝ Otherwise, InvalidClassException.
The garbage collection APIs in the
java.lang.refpackage and thejava.util.WeakHashMapclass cannot be used with value objects. Attempting to createReferenceobjects for value objects will cause anIdentityExceptionto be thrown.
If your operations depend on an object’s identity, you can:
check with Objects::hasIdentity
verify with Objects::requireIdentity
Everything else works as for identity classes, too, e.g.:
Methods that operate on
ObjectorObject[]parameters accept value objects. Almost anywhere you need to provide an implementation of an interface, the implementation may be a value class. Generic types such asList<T>andComparable<T>can be instantiated with value classes as the type arguments.
Behavioral differences vs identity classes:
new may not allocate a fresh object
== is field-wise comparison
Behavioral restrictions:
no deep reflection
no object monitor interaction
no finalization
limited serialization (records or proxies)
no GC interaction
What other benefits can we get?
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
As a general rule, if a class with immutable state does not require identity then it is probably appropriate to adopt the
valuemodifier. This includes abstract classes.
Keep value class APIs small:
enough logic to construct, validate, and transform instances
To test whether two value objects represent the same value, use the
equalsmethod. When declaring a class, defineequalsin a way that always returnstruefor interchangeable instances.
Values aren’t records:
values give up identity for
safety and performance
records give up encapsulation for
succinctness and usability
But many classes need neither
identity nor encapsulation.
Considerable overlap between values and records:
both immutable
both (often) represent simple data
both (often) have simple APIs
⇝ Value records will be very common
(probably the norm).
| Object Identity |
| Value Types |
| Declaration |
| Behavior |
| Recommendations |
| Optimization |
Value classes have performance implications,
but value should not be used as a "speed-up keyword".
JVM applies optimizations
these depend on many factors
they will evolve over time
Optimizing value objects will become part
of regular JDK performance work.
At run time, a JVM can optimize value objects by encoding references to them in more compact forms than references to identity objects. Instead of allocating space in the heap for a value object, a JVM can flatten and scalarize the reference to the object.
When a field of one object, or an element of an array, stores a reference to another object, a JVM can encode the other object’s field values directly into the reference. When this is done, the reference is not a pointer to the other object in memory. The reference is said to be flattened.
This usually requires an extra bit to encode nullity.
An array of
LocalDatereferences can be flattened by prepending anullflag to the year-month-day triple of aLocalDateobject (anintand twobyte-s).
+--------------+
| LocalDate[5] |
+--------------+
| 1|2026|08|16 |
| 1|2026|08|17 |
| 1|2026|08|18 |
| 0|0000|00|00 |
| 1|2026|08|20 |
+--------------+class Event {
private LocalDate day;
}# POINTER REFERENCE ║ # FLATTENED REFERENCE
┌ ║ ┌
| EVENT ┌ ║ | EVENT
| day -> | LOCALDATE ║ | day[1|2026|08|19]
└ | 2026|08|19 ║ └
└ ║Values still need to be written atomically.
This is generally limited to 64 bits.
Due to the nullity bit, this limits reference flattening
to values classes with total fields <64 bits. 😕
Flattening would be more applicable with:
128-bit atomic writes
language-level atomicity control
language-level nullity information
generic specialization (JEP 218)
When passing objects to or from a method,
the runtime generally references heap objects.
For identity objects, escape analysis can prove
that the identity doesn’t need to be tracked.
⇝ The runtime can load fields once
and use the stack to pass them.
Value objects lack identity,
so this can always be done.
Unlike for flattening,
no strict boundary exists.
Reference flattening and scalarization are optimizations, not language features. You cannot directly control them. Like all optimizations, they are done at the discretion of the JVM. There are, however, things you can do to make it more likely that a JVM can apply these optimizations.
Only references that are declared as value classes can be optimizied, e.g.:
// flattenable
Integer[] ints = { 1996, 2006, 1996, null, null };
// not flattenable
Object[] objs = { 1996, 2006, 1996, null, null };
// field is not flattenable
record Box<T>(T field) { }
// field stores a heap pointer
var b = new Box<LocalDate>(i);Optimization of value objects:
JVM can flatten and scalarize some references
flattening is limited to small values
scalarization is more universally applicable
As (almost) always in Java:
write maintainable code
use constructs semantically
rely on JVM for optimization
question-mark: Milos Milosevic (CC-BY 2.0)