Summary
Introduce value objects, which are immutable and lack object identity. Value objects are distinguished solely by the values of their fields, and can be represented by Java Virtual Machines in ways that improve performance. This is a preview language and VM feature.
Goals
-
Enable developers to opt in to a programming model for immutable data in which the
==operator, and all other operations, distinguish objects by the values of their fields rather than their identities. -
Support the compatible migration of existing classes that represent immutable data to this model. Migrate suitable existing classes in the Java Platform API, such as
IntegerandLocalDate, to have value object instances. -
Do not ask developers to learn new semantics for memory management or variable storage. The Java language should continue to operate on just two kinds of data: primitives and object references.
-
Maximize the freedom of JVM implementors to represent immutable data in ways that improve memory footprint, locality, and garbage collection efficiency.
Non-Goals
-
It is not a goal to automatically treat instances of existing classes as value objects. Value objects do not uniformly work the same way as other objects, so class authors must explicitly choose to have value object instances.
-
It is not a goal to revise the
==operator so that it can be used in place of theequalsmethod. We redefine==only as much as necessary to cope with a new kind of identity-free object. The usual advice to compare objects in most contexts using theequalsmethod still applies. -
It is not a goal to introduce a
structfeature, as found in the C and C# languages. -
It is not a goal to change the treatment of primitive types. Primitives behave like value objects in many ways, but are a distinct concept.
-
It is not a goal to guarantee any particular optimization strategy or memory layout. This proposal enables many optimizations, but we will implement only some of them initially. Some optimizations, such as layouts that exclude
null, will only become possible after future language and JVM enhancements.
Motivation
Many kinds of simple data values are immutable: complex numbers, pixel colors,
times, dates, and so on. We usually model such values with classes that contain
just enough logic to construct, validate, and transform instances, and that
define the equals, hashCode, and toString methods so that equivalent
instances can be used interchangeably.
As an example, the Platform API's LocalDate class models dates:
jshell> LocalDate d1 = LocalDate.of(1996, 1, 23)
d1 ==> 1996-01-23
jshell> LocalDate d2 = d1.plusYears(30)
d2 ==> 2026-01-23
jshell> LocalDate d3 = d2.minusYears(30)
d3 ==> 1996-01-23
jshell> d1.equals(d3)
$4 ==> true
Intuitively, the essence of a LocalDate object rests in its year, month, and
day values. But in the Java language, the essence of any object is its
identity. Each time the LocalDate.of method invokes new LocalDate(...),
the JVM creates a new object with a unique identity, distinguishable from every
other object in the system.
The easiest way to observe the identity of an object is with the == operator:
jshell> d1 == d3
$6 ==> false
Even though d1 and d3 represent the same year-month-day triple — that is,
d1.equals(d3) is true — they are two objects with distinct identities.
Immutable data does not need identity
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.
Consider a text-editing application in which lines of text are represented by
instances of a Line class. The sole field of the class is a list of
characters, which is mutated when the user edits the line. Two Line objects
might contain equivalent character lists, and thus also be equivalent, but that
would be a coincidence; when the user changes one of the lines, the application
will mutate the character list of that object but not the other, relying on
identity to mutate the right one.
In other words, when objects are mutable, they cannot be interchangeable — yet
most immutable data values are interchangeable. There is no practical difference
between two LocalDate objects representing 1996-01-23, because their state
is fixed and unchanging. They represent the same value, both now and in the
future. There is no need to distinguish the two objects via their identities.
Object identity is, in fact, actively confusing when objects are immutable and
interchangeable. Most of us can recall the experience of unwittingly using ==
to compare objects, as in d1 == d3 above, and being mystified by a false
result even though the objects' state and behavior seem identical.
Even worse, object identity can expose incidental implementation choices that
result in surprising behavior. For example, the Integer class uses a
cache to avoid creating unnecessary Integer objects with
unique identities. There is, e.g., typically just a single Integer object
representing the value 1. The cache is of fixed size, however, and does not
extend to larger int values such as 1996:
jshell> Integer i = 1, j = 1;
i ==> 1
j ==> 1
jshell> i == j
$3 ==> true
jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996
jshell> x == y
$6 ==> false
We could avoid this sort of unexpected outcome if objects whose state and behavior make them interchangeable could be freed from the legacy requirement to have distinct identities.
Object identity is expensive at run time
The Java language's requirement that every object have identity, whether needed or not, is a performance impediment. It forces JVMs to allocate memory for each newly created object, thereby distinguishing that object from every other object already in the system, and access that memory whenever the object is used.
For example, suppose that a program creates arrays of int values and
LocalDate references:
jshell> int[] ints = { 1996, 2006, 1996, 1, 23 }
ints ==> int[5] { 1996, 2006, 1996, 1, 23 }
jshell> LocalDate[] dates = { d1, d1, d2, null, d3 }
dates ==> LocalDate[5] { 1996-01-23, 1996-01-23, 2026-01-23,
null, 1996-01-23 }
The int array can be represented by a single block of memory containing int
values:
+----------+
| int[5] |
+----------+
| 1996 |
| 2006 |
| 1996 |
| 1 |
| 23 |
+----------+
The LocalDate array, by contrast, must be represented by a block of memory
containing a sequence of pointers, each referencing another block of memory
representing a LocalDate object:
+--------------+
| LocalDate[5] |
+--------------+
| 87fa1a09 | -----------------------> +-----------+
| 87fa1a09 | -----------------------> | LocalDate |
| 87fb4ad2 | ------> +-----------+ +-----------+
| 00000000 | | LocalDate | | y=1996 |
| 87fb5366 | --- +-----------+ | m=1 |
+--------------+ | | y=2026 | | d=23 |
v | m=1 | +-----------+
+-----------+ | d=23 |
| LocalDate | +-----------+
+-----------+
| y=1996 |
| m=1 |
| d=23 |
+-----------+
Even though the data represented by the LocalDate array is not significantly
more complex than the int array — a year-month-day triple is effectively 48
bits of primitive data — the memory footprint is far greater because of the
pointers and allocated objects.
To make matters worse, when a program iterates over the LocalDate array, it
may dereference each pointer. Modern CPUs improve performance by caching small
chunks of memory called
cache lines.
The various LocalDate objects could be allocated at memory addresses that are
far apart if, e.g., they were created at different times, or they were moved by
the garbage collector. The resulting poor
reference locality
could degrade performance by requiring every dereference to load a different
cache line from memory.
Out of desperation, we might try to improve performance by writing code that
creates as few objects as possible, thereby de-stressing the garbage collector
and improving reference locality. For example, rather than use LocalDate
objects we could model dates with int values counting the number of days
since 1970-01-01. Unfortunately, this approach gives up the features
of classes that make Java code so maintainable: meaningful names, private state,
data validation by constructors, convenience methods, and so forth. It would be
all too easy to forget — or for a colleague simply not to know — that int
dates are relative to 1970-01-01 rather than some other date, leading to bugs
that are difficult to diagnose.
Programming without identity
Trillions of Java objects are created every day, each one bearing a unique
identity. We should enable developers to choose which objects in a program need
identity, and which do not. The author of a class such as LocalDate, which
represents simple immutable data, should be able to opt out of identity. Two
LocalDate objects representing the date 1996-01-23 should be
indistinguishable, just as two int values representing the number 4 are
indistinguishable.
By opting out of identity, developers opt in to a programming model that enables the best of both worlds: the abstraction of classes, with the simplicity and performance benefits of primitives.
In the future, this programming model will support new Java Platform APIs, such as classes that encode different kinds of integers and floating-point values, and new Java language features, such as user-defined conversions and mathematical operators for immutable data.
Description
We introduce value objects to model simple immutable data. A value object is
an instance of a value class, declared with the value modifier. Classes
without the value modifier are identity classes, and their instances are
identity objects.
Java programs manipulate objects through references. A reference to an object
is stored in a variable and enables us to find the object's fields.
Traditionally, references are represented in a JVM as pointers to memory
locations, thus encoding the unique identity of each object. Each invocation of
the new operator allocates a fresh object, in a fresh block of memory, and
returns a unique reference. And, traditionally, the == operator compares
references by comparing pointers, so distinct references to two objects are not
== even if the referenced objects are interchangeable.
Value objects are different. A reference to a value object is stored in a
variable and enables us to find the object's fields. In a JVM, however, it might
not be represented by a pointer and thus does not encode the unique identity of
the object. For a value class, invoking the new operator might not allocate a
fresh object; it might, instead, return a reference to an existing object, or
even a reference that embodies the object directly. The == operator compares
references to value objects by comparing the objects' field values, so
references to two objects are == if the objects have identical field values.
We can save memory and improve performance by using value objects for immutable
data. Because a program cannot distinguish two value objects with identical
field values, not even with the == operator, a JVM is able to change how a
value object is laid out in memory without affecting the program. A JVM could,
e.g., store the fields of a value object on the stack or even in CPU registers,
rather than the heap.
Value objects are a preview language and VM feature, disabled by default
To use this feature in JDK 28, you must enable preview features:
-
Compile the program with
javac --release 28 --enable-preview Main.javaand run it withjava --enable-preview Main; or, -
When using the source code launcher, run the program with
java --enable-preview Main.java; or, -
When using jshell, start it with
jshell --enable-preview.
Some classes in the Java Platform API become value classes only when preview features are enabled; otherwise, they behave just as they did in JDK 27.
For example, if your code refers to the LocalDate class and you compile with
preview features disabled, the compiler uses the existing identity-object
version of LocalDate and you do not need to run the program with preview
features enabled. If you compile with preview features enabled, however, the
compiler uses the new value-object version of LocalDate and you must run the
program with preview features enabled. It is not possible to use the
identity-object version of LocalDate when preview features are enabled.
Contents
Programming with value objects
In the Java Platform API, 30 classes are now declared as value classes. Examples include:
| Package | Classes |
|---|---|
| java.lang | Integer,
Long,
Float,
Double,
Byte,
Short,
Character,
Boolean
|
| java.util | Optional,
OptionalInt,
OptionalLong,
OptionalDouble
|
| java.time | LocalDate,
LocalTime,
LocalDateTime,
ZonedDateTime,
Duration
|
All instances of these classes are value objects. This includes the boxed
primitives that are instances of Integer, Long, and so
forth. The == operator compares value objects by their field values, so, e.g.,
two Integer objects are == if they represent the same primitive value:
$ jshell --enable-preview
| Welcome to JShell -- Version 28-internal
| For an introduction type: /help intro
jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996
jshell> x == y
$3 ==> true
Similarly, two LocalDate objects are == if they have the same year, month,
and day values:
jshell> LocalDate d1 = LocalDate.of(1996, 1, 23)
d1 ==> 1996-01-23
jshell> LocalDate d2 = d1.plusYears(30)
d2 ==> 2026-01-23
jshell> LocalDate d3 = d2.minusYears(30)
d3 ==> 1996-01-23
jshell> d1 == d3
$7 ==> true
We can use the new Objects.hasIdentity method to observe whether an object is
an identity object:
jshell> Objects.hasIdentity(d1)
$8 ==> false
The String class, due to some dependencies on object identity in its API and
implementation, is not a value class, so instances of String are always
identity objects:
jshell> String s = "abcd"
s ==> "abcd"
jshell> Objects.hasIdentity(s)
$10 ==> true
jshell> String t = "aabcd".substring(1)
t ==> "abcd"
jshell> s == t
$12 ==> false
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.
A few identity-sensitive operations, however, are not supported by value objects, including synchronization:
jshell> synchronized (d1) { d1.notify(); }
| Error:
| unexpected type
| required: a type with identity
| found: java.time.LocalDate
| synchronized (d1) { d1.notify(); }
| ^--------------------------------^
jshell> Object o = d1
o ==> 1996-01-23
jshell> synchronized (o) { o.notify(); }
| Exception java.lang.IdentityException: Cannot synchronize on
an instance of value class java.time.LocalDate
| at (#19:1)
JVM implementors have the freedom to encode references to value objects at run time in ways that optimize memory footprint, locality, and garbage collection efficiency. For example, we saw the following array earlier, implemented with pointers to heap objects:
jshell> LocalDate[] dates = { d1, d1, d2, null, d3 }
dates ==> LocalDate[5] { 1996-01-23, 1996-01-23, 2026-01-23,
null, 1996-01-23 }
Now that LocalDate objects lack identity, a JVM need not represent references
to them using pointers; it can, rather, encode the fields of LocalDate objects
directly in the references. Each element of the dates array can be represented
as a 64-bit word that indicates whether the reference is null and, if not,
directly stores the year, month, and day field values of the value object:
+--------------+
| LocalDate[5] |
+--------------+
| 1|1996|01|23 |
| 1|1996|01|23 |
| 1|2026|01|23 |
| 0|0000|00|00 |
| 1|1996|01|23 |
+--------------+
The performance characteristics of this LocalDate array may be similar to
those of an ordinary int array, with lower memory footprint and better
reference locality:
+----------+
| int[5] |
+----------+
| 1996 |
| 2006 |
| 1996 |
| 1 |
| 23 |
+----------+
This optimization is just one example; some value classes, such as
LocalDateTime, are too large to take advantage of this particular technique.
Still, the lack of identity enables JVM implementors to optimize references to
value objects in many ways.
Declaring value classes
You can declare your own value classes by applying the value modifier 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.
When the value modifier is applied to a class, the fields of the class are
implicitly final. The class itself is also implicitly final, so it cannot be
extended. Because the class is final, its methods cannot be overridden.
There is no restriction on the types of fields in a value class. The fields may store references to other value objects, or to identity objects, e.g., strings.
Record classes are final and all their fields are final, so they
are often 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)
$3 ==> false
jshell> new Point(17, 3) == p
$4 ==> true
Many classes have immutable and interchangeable instances, but they cannot be
record classes because their fields do not correspond exactly to their
constructor arguments; i.e., they are not transparent. Such classes might use
private fields internally in a more efficient way than is exposed externally
through public methods. For example, a class might represent a quantity of euros
and cents with a single long field to save memory; it cannot be a value
record, but it can still be a value class:
value class EURCurrency {
private long cs; // implicitly final
private EURCurrency(long cs) { this.cs = cs; }
public EURCurrency(long e, int c, boolean neg) {
this(neg ? -e * 100 - c : e * 100 + c);
}
public EURCurrency(long e, int c) { this(e, c, false); }
public long euros() { return Math.abs(cs) / 100; }
public int cents() { return (int) Math.abs(cs) % 100; }
public boolean negative() { return cs < 0; }
public String toString() {
var prefix = negative() ? "-€" : "€";
return "%s%d,%d".formatted(prefix, euros(), cents());
}
}
Comparing value objects
Traditionally, the purpose of the == operator was to test whether two
referenced objects have the same identity.
After the introduction of value objects, the purpose of the == operator is to
test whether two referenced objects are indistinguishable. Since identity
objects are, by definition, distinguished by their identities, this means that
the == operator works the same for identity objects in Java 28 as it has since
Java 1.0: It tests for references to the same object — at the same location in
memory — or for matching null references.
When comparing two value objects, the == operator tests for references to
instances of the same class with the same field values. That is, two value
objects are indistinguishable if:
-
They are instances of the same value class,
-
Their primitive-typed fields store the same bit patterns, and
-
Their reference-typed fields are indistinguishable, applying the
==operator recursively.
When that is the case, a JVM can freely replace one reference with the other, and no code will be able to tell the difference.
The == operator and the equals method will often produce the same results
for value objects. For some value classes, however, instances may be
interchangeable (i.e., equals) even if their field values are different (i.e.,
not ==). To test whether two value objects represent the same value, use the
equals method. When declaring a class, define equals in a way that always
returns true for interchangeable instances.
The Substring value class, below, illustrates how == and equals may differ
for some value objects. This class represents a substring of a string without
allocating a new char[] in memory. The internal state of a Substring
instance is a source string and two coordinates, while the value represented by
the instance is a character sequence, as produced by toString. Accordingly,
two instances may represent the same character sequence (i.e., are equals)
even though their internal state is different (i.e., not ==).
value class Substring {
private String str;
private int start, end;
public Substring(String s, int i, int j) {
str = s; start = i; end = j;
}
public String toString() {
return str.substring(start, end);
}
public boolean equals(Object o) {
return o instanceof Substring && toString().equals(o.toString());
}
public int hashCode() {
return Objects.hash(Substring.class, toString());
}
}
jshell> Substring sub1 = new Substring("ringing", 1, 4);
sub1 ==> ing
jshell> Substring sub2 = new Substring("ringing", 4, 7);
sub2 ==> ing
jshell> sub1.equals(sub2)
$3 ==> true
jshell> sub1 == sub2
$4 ==> false
The results of the == operator and the equals method may also be different
if the fields of two value objects refer to distinct identity objects that are
interchangeable according to equals:
jshell> String r = "bringing".substring(1);
r ==> ringing
jshell> r == "ringing"
$6 ==> false
jshell> Substring sub3 = new Substring(r, 1, 4);
sub3 ==> ing
jshell> sub1.equals(sub3)
$8 ==> true
jshell> sub1 == sub3 // tests sub1.str == sub3.str
$9 ==> false
Another situation in which the == operator and equals may differ is when
value objects have float or double fields. The primitive floating-point
types support multiple NaN values. These NaN values are treated as
interchangeable by most floating-point operations, but because each value is
distinct, value objects that wrap different NaN values are distinguishable by
the == operator. When declaring a value class, you must decide whether that
distinction is meaningful for the equals method. For example, in a value
record, the default behavior of equals treats all NaN values as
interchangeable:
jshell> value record Length(float val) {}
| created record Length
jshell> Length l1 = new Length(Float.intBitsToFloat(0x7ff80000))
l1 ==> Length[val=NaN]
jshell> Length l2 = new Length(Float.intBitsToFloat(0x7ff80001))
l2 ==> Length[val=NaN]
jshell> l1.equals(l2)
$4 ==> true
jshell> l1 == l2
$5 ==> false
jshell> Integer.toHexString(Float.floatToRawIntBits(l1.val()))
$6 ==> "0x7ff80000"
jshell> Integer.toHexString(Float.floatToRawIntBits(l2.val()))
$7 ==> "0x7ff80001"
(For more on the different kinds of equivalence between floating-point values,
see the specification of the Double class.)
Because the == operator compares the reference-typed fields of value objects
recursively, applying it to two value objects may require an unbounded number of
comparisons. In the following example, two deep nests of Box objects must be
fully traversed to determine whether the objects are indistinguishable:
jshell> value record Box(Object val) {}
| created record Box
jshell> var b1 = new Box(new Box(new Box(new Box(l1))))
b1 ==> Box[val=Box[val=Box[val=Box[val=Length[val=NaN]]]]]
jshell> var b2 = new Box(new Box(new Box(new Box(l2))))
b2 ==> Box[val=Box[val=Box[val=Box[val=Length[val=NaN]]]]]
jshell> b1.equals(b2)
$11 ==> true
jshell> b1 == b2
$12 ==> false
Constructors of value classes are constrained, as discussed
below, so that the recursive application
of the == operator to value objects will never cause an infinite loop. But
deep comparisons may take a long time, or even trigger a StackOverflowError.
Value classes and subclassing
Every value class belongs to a class hierarchy with java.lang.Object at its
root, just like every identity class. There is no java.lang.Value superclass
of all value classes.
Value classes can implement interfaces. Thus variables declared with an
interface type, or with the type Object, can store references to both value
objects and identity objects:
jshell> Comparable<?> comp = LocalDate.of(1996, 1, 23)
comp ==> 1996-01-23
jshell> Objects.hasIdentity(comp)
$2 ==> false
jshell> comp = "abc"
comp ==> "abc"
jshell> Objects.hasIdentity(comp)
$4 ==> true
By default, a value class is implicitly final and cannot be extended. A value
class may, however, be declared abstract, allowing it to be extended by other
classes and have its methods overridden. The fields of an abstract value class
are implicitly final, as in a concrete value class. The methods of an abstract
value class may be marked abstract, as in an abstract identity class.
Declaring an abstract value class is an indication that the class itself has no
need for identity. Its subclasses may be value classes or identity classes. (The
value modifier on an abstract value class can be read as meaning
value-compatible.)
A value class can extend either java.lang.Object or an abstract value class,
but not an identity class. (The Object class is unique in this respect: It is
neither abstract nor a value class, and instances produced by new Object()
have identity, yet it also permits extension by value classes.)
Many existing abstract classes, if they are designed to be publicly extensible,
are good candidates to be abstract value classes. For example, the abstract
class Number has no fields, nor any code that depends on identity-sensitive
features, so it can safely be migrated to an abstract value class:
abstract value class Number implements Serializable {
public abstract int intValue();
public abstract long longValue();
public byte byteValue() { return (byte) intValue(); }
...
}
Integer, which is a value class, and java.math.BigInteger, which is an
identity class, both extend Number:
jshell> Number num = 123
num ==> 123
jshell> Objects.hasIdentity(num)
$2 ==> false
jshell> num = BigInteger.valueOf(123)
num ==> 123
jshell> Objects.hasIdentity(num)
$4 ==> true
An abstract value class can be sealed in order to limit the classes that can extend the class:
sealed abstract value class UserID
permits EmailID, PhoneID, UsernameID
{
...
}
value class EmailID extends UserID {
private String name, domain; ...
}
value class PhoneID extends UserID {
private String digits; ...
}
value class UsernameID extends UserID {
private String name; ...
}
Safe construction of value objects
Constructors initialize newly-created objects by setting the values of their fields. Because value objects do not have identity, they must only be observed by other code with their field values fully initialized.
To ensure this is true, the fields of a value object must be set before the
object is shared with other code. This also prevents any two value objects from
referring to each other, either directly or indirectly, thereby ensuring that
invoking the == operator never results in an infinite loop.
An object in the process of being constructed is larval — it has been created,
but is not yet fully formed. Larval objects should always be handled carefully:
If a larval object is shared with code outside the constructor then invariant
properties of the object may not yet hold, and that code may even observe the
mutation of final fields.
Traditionally, a constructor begins the initialization process by invoking a
superclass constructor, super(...). If a constructor does not do this
explicitly then the Java compiler inserts a super() call at the beginning of
the constructor's body. After the superclass constructor returns, the subclass
constructor proceeds to set its declared instance fields and perform other
initialization tasks. This pattern exposes an object with completely
uninitialized subclass fields to larval object leakage in any superclass
constructor.
Flexible constructor bodies, introduced in Java 25, enable
safer initialization by allowing fields to be set and other code to be executed
before the super(...) invocation. With this feature, there are two phases to
the object initialization process: early construction, before the super(...)
invocation, and late construction, afterwards.
During the early construction phase, larval object leakage is impossible: The
constructor may set the fields of the larval object, but may not invoke instance
methods or otherwise make use of this. Fields that are initialized in the
early construction phase are therefore set before they can ever be read, even if
a superclass leaks the larval object. Final fields, in particular, can never be
observed to be mutated.
In a value class, constructor code always runs in the early construction phase.
The Java compiler inserts a super() call at the end of the constructor body,
not the beginning. Attempts to invoke instance methods or otherwise use this
will fail:
value class Name {
String name;
int length;
private int strLength() {
return name.length();
}
Name(String n) {
name = n;
length = strLength(); // Error, invokes this.strLength()
}
}
Instance fields that are declared with initializer expressions are set at the start of the constructor, in the early construction phase. Instance initializer blocks, a rarely-used feature, are run in the late construction phase; they are not allowed to set the instance fields of value classes.
If a value class constructor contains code that needs to work with this, an
explicit super(...) or this(...) call can be used to mark the transition
from the early construction phase to the late construction phase. Before
super(...) can be called, however, all the class's fields must be assigned,
without referring to this:
value class Name {
String name;
int length;
private static int strLength(String n) {
return n.length();
}
Name(String n) {
name = n;
length = strLength(name); // OK, strLength is now static
super(); // All fields must be set at this point
System.out.println("Name: " + this);
}
}
Other construction enhancements
We loosen some of the restrictions on accessing the fields of all larval
objects, whether they are value objects or identity objects. In Java 27, a new
object's fields could be set in the early construction phase, but not read. In
Java 28, with preview features enabled, these fields can be both written and
read in the early construction phase. It continues to be illegal to refer to
inherited fields, invoke instance methods, or share this with other code until
the late construction phase.
In addition, for simplicity and improved performance, in Java 28, with preview features enabled, all record classes, whether value records or identity records, adopt the same safe construction rules as value classes. A record class's constructor always runs in the early construction phase. The fields of a record must be set before they can be observed, thus users of the class can always expect component accesses to produce consistent results.
This change is not source compatible for identity record constructor
declarations, though we expect incompatibilities to be rare in practice. For
example, this record class fails to compile because its canonical constructor
refers to this in the early construction phase:
record Node(String label, List<Node> edges) {
static void nullCheck(Object arg, Object owner) {
if (arg == null) {
String msg = "null arg for " + owner.toString();
throw new IllegalArgumentException(msg);
}
}
public Node {
nullCheck(label, this); // Error with --enable-preview
nullCheck(edges, this); // Error with --enable-preview
}
}
Such incompatibilities will be rare because, as illustrated here, most attempts
to use this in a record constructor are bugs — the fields of a Node are not
yet set when this code calls toString(). A survey of existing record class
declarations in a large source-code corpus confirms that it is rare for record
classes to violate the new rules.
If a record constructor legitimately must access this then you can insert an
explicit super() invocation, but you must explicitly set the record's fields
before that, in the early construction phase.
Inherited methods of java.lang.Object
Like any class, a value class inherits methods including equals, hashCode,
and toString from java.lang.Object, unless overridden. These methods
traditionally depend on identity, but when operating on a value object, they use
the values of the object's fields instead. Specifically:
-
The inherited implementation of
Object.equalsuses the==operator to test whether the objects are indistinguishable. This might be the rightequalsbehavior for a value class, but if it is not then the class should override theequalsmethod. -
The inherited implementation of
Object.hashCodecomputes a hash from the object's field values. (This value can also be computed viaSystem.identityHashCode— an unfortunate legacy name.) As usual, if a value class overridesequalsthen it should also overridehashCode. -
The inherited implementation of
Object.toStringreturns a string of the form"ClassName@hashCode". Since value classes represent immutable data, they should overridetoStringto more legibly convey the values represented by their instances.
In a value record, as for all records, the default behavior of the equals,
hashCode, and toString methods is to recursively apply the same operations
to the record's components.
A few other methods of Object interact with value objects:
-
For a
Cloneablevalue class, theObject.clonemethod produces a value object that is indistinguishable from the original. The usual expectation thatx.clone() != xis not meaningful for value objects. When declaring a value class that holds references to identity objects, consider overriding theclonemethod to create deep copies of those objects. -
The
waitandnotifymethods require that the object be locked in the current thread. Since it is impossible to synchronize on a value object, attempts to call these methods always fail with anIllegalMonitorStateException. -
The
finalizemethod of a value object is never invoked by the garbage collector.javacissuesidentitywarnings for value classes that overridefinalize.
Migrating to value classes
Value classes and records are useful tools for any class that models simple immutable data.
As a general rule, if a class with immutable state does not require identity
then it is probably appropriate to adopt the value modifier. This includes
abstract classes, which often have no state at all and should not impose an
unnecessary identity requirement on their subclasses. (Some abstract classes
define an inherently mutable API, even if they declare no mutable state, and so
should not be made value classes. Some concrete classes do not model data at all
— for example, they may be designed to be instantiated only once.)
If a class is either final or abstract, and has only final fields, then
adding or removing the value keyword is a binary-compatible change.
Migrating an identity class to a value class does present source and behavioral incompatibility risks worth considering:
-
If the class has public constructors, existing clients may rely on them to create objects that are known to be distinguishable from every other object via the
==operator. Changing the class to be a value class will invalidate that logic, possibly leading to run-time bugs.If this incompatibility is a serious concern, it may be appropriate to deprecate the public constructors and encourage the use of factory methods instead. As an example, in Java 9 we deprecated the constructors of
Integer,Long, etc., recommending the use of the corresponding factory methodsInteger.valueOf,Long.valueOf, etc., instead. -
If existing clients synchronize on instances of the class then after migration they will fail, either with a compile-time error or an
IdentityExceptionat run time. This incompatibility is more likely to be a risk for classes with public constructors, since clients may rely on those constructors to create unique instances for locking. -
If the
equalsandhashCodemethods have not already been overridden, they will behave differently after migration. A good migration candidate will override these methods prior to migration so that their behavior does not depend on identity. -
If the class encapsulates sensitive data, be cautious about the risk of exposing that data via the
==operator orSystem.identityHashCode. A malicious user could use these operations to try to infer the data inside an instance. Value objects are not designed to protect sensitive data against such attacks. -
If the class implements
Serializablethen it may require special handling, as discussed below. Migrating it to a value class may, moreover, break certain uses of the reflection and garbage collection APIs, also discussed below, or other specialized identity-sensitive code.
Value classes in the Java Platform
In the Java Platform API, 30 classes are now declared as value classes:
| Package | Classes |
|---|---|
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
|
To minimize compatibility risks, the specifications of these classes have long discouraged relying on the identities of their instances, and have long been specified as value-based. They have also discouraged or even prevented instance creation via constructors. Since Java 16, warnings for value-based classes have discouraged synchronizing on instances of these classes.
The vast majority of the Platform APIs work seamlessly with value objects.
Methods that operate on Object or Object[] 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 as List<T> and
Comparable<T> can be instantiated with value classes as the type arguments.
Additional changes in the Platform APIs further support value objects:
-
Two new methods in the
java.util.Objectsclass,hasIdentityandrequireIdentity, allow you to distinguish between identity objects and value objects. -
A new constant in
java.lang.reflect.AccessFlag,IDENTITY, tells when a class is an identity class.Whether a class is an identity class or a value class is recorded in its class file. Identity classes have the
ACC_IDENTITYflag set; value classes do not. This flag supersedes the legacyACC_SUPERflag. The JVM Specification has always recommended that compilers and tools set theACC_SUPERflag in class files, so, by default, compilers and tools will continue to set the flag in new class files and thus generate identity classes. -
Serialization of value records works automatically, but 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. If these methods are not implemented, attempts to serialize or deserialize the value object will fail with anInvalidClassException.These methods must be implemented because value classes are compiled using strictly-initialized fields, and deserialization cannot safely initialize such fields. Value objects may only be created, and their fields initialized, by invoking a constructor. In the future, we expect to enhance the serialization mechanism so that serializable value classes can be serialized and deserialized automatically.
javacissuesserialwarnings for value classes that will fail serialization at run time. -
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 that modifyfinalfields via deep reflection are incompatible with safe construction. They are not permitted to modify the fields of value objects even if the--enable-final-field-mutationoption is given on the command line. Libraries must initialize instances of a value class using the class's constructors. -
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.Since JDK 25,
javachas issuedidentitywarnings when value-based classes are used with these APIs. As of JDK 28,javacalso issuesidentitywarnings when value classes are used with these APIs.
Run-time optimizations for value objects
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.
-
Reference flattening: 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.
-
Reference scalarization: When a method parameter or local variable stores a reference to an object, a JVM can encode the object's field values into additional local variables. When this is done, again, the reference is not a pointer to the object in memory. The reference is said to be scalarized.
When a reference is flattened or scalarized, it needs no independent object representation in the heap. This means it has no impact on garbage collection, and its data is always co-located in memory with the referencing object or call stack.
Reference flattening
As an example, a JVM could flatten an array of Integer references so that each
array element holds a reference that directly encodes the underlying integer
value instead of pointing to the memory location of an Integer object. Each
reference also indicates whether the original Integer reference was null by
prepending a 0 (null) or 1 (non-null) flag to the integer value:
+--------------+
| Integer[5] |
+--------------+
| 1|1996 |
| 1|2006 |
| 1|1996 |
| 0|0 |
| 0|0 |
+--------------+
Each int value takes up 32 bits, and each null flag requires at least one
additional bit. Due to hardware constraints, a JVM will probably encode each
flattened Integer reference as a 64-bit word. An Integer array thus has a
larger memory footprint than a plain int array, but a significantly smaller
total footprint than an array of pointers to Integer objects. (Each pointer is
a 32- or 64-bit value, and each referenced object requires at least 64
bits just for its header.) Even more significantly, all of the
Integer data is stored directly inside the array, where it can be accessed
without any additional memory loads.
As shown earlier, an array of LocalDate references can be flattened by
prepending a null flag to the year-month-day triple of a LocalDate object
(an int and two bytes). Like flattened Integer references, these flattened
LocalDate references can fit in 64 bits:
+--------------+
| LocalDate[5] |
+--------------+
| 1|1996|01|23 |
| 1|1996|01|23 |
| 1|2026|01|23 |
| 0|0000|00|00 |
| 1|1996|01|23 |
+--------------+
Fields can also store flattened references. For example, a LocalDateTime
object has two fields, a LocalDate and a LocalTime, and each can store a
flattened reference:
+----------------------+
| LocalDateTime |
+----------------------+
| date=1|2026|01|23 |
| time=1|09|00|00|0000 |
+----------------------+
Reference flattening must maintain the integrity of data. A flattened reference must always be read and written atomically, or it could become corrupted. On common hardware architectures, this limits the size of mutable fields that store flattened references to no more than 64 bits.
For example, attempting to flatten a reference to a LocalDateTime object would
embed fields from the underlying LocalDate and LocalTime objects, plus a
null flag for each, plus a null flag for the LocalDateTime itself. The
flattened reference would likely be too big to read and write atomically, so a
JVM could not store it in a mutable field of type LocalDateTime, such as the
lastClicked time of an identity class Button:
+--------------------------------------------+
| Button |
+--------------------------------------------+
| lastClicked=1|1|2026|01|23|1|09|00|00|0000 | // Not possible
| ... |
+--------------------------------------------+
Instead, the JVM would choose — silently, at its discretion — a reference layout
compatible with a mutable lastClicked field. Perhaps the field would store a
pointer to a LocalDateTime object, whose own fields may store flattened
references as shown earlier:
+----------------------+
| Button |
+----------------------+
| lastClicked=87fa50a0 |------> +----------------------+
| ... | | LocalDateTime |
+----------------------+ +----------------------+
| date=1|2026|01|23 |
| time=1|09|00|00|0000 |
+----------------------+
The fields of a value class, by contrast, do not have this atomicity limitation,
since the fields of value objects can never be observed to be mutated. Thus, for
example, the timestamp field of an Event value class could store a flattened
reference to a LocalDateTime object:
+------------------------------------------+
| Event |
+------------------------------------------+
| timestamp=1|1|2026|01|23|1|09|00|00|0000 | // OK in a value class
| ... |
+------------------------------------------+
Future enhancements may enable more flattening of references to 64-bit and even larger value objects. For example, additional language features may allow value classes to opt out of atomicity constraints, or perhaps 128-bit atomic mutable fields will become viable on some hardware architectures.
Reference scalarization
When a JVM loads a flattened reference from the field of an object in the heap, it must decode the reference into a form that it can readily work with. For code compiled by the JVM's just-in-time (JIT) compiler, this form can be a scalarized reference.
For example, consider this code fragment:
LocalDate d = dates[0];
dates[0] = d.plusYears(30);
The LocalDate.plusYears method itself might be declared:
public LocalDate plusYears(long yearsToAdd) {
int newYear = YEAR.checkValidIntValue(this.year + yearsToAdd);
return new LocalDate(newYear, this.month, this.day);
}
In pseudo-code, the result of JIT-compiling the plusYears method might look
like the following, using the notation { ... } to indicate that multiple
values are returned from a JIT-compiled method (this is purely notational; there
is no wrapper at run time):
static { boolean, int, byte, byte }
plusYears(boolean this_null,
int this_year, byte this_month, byte this_day,
long yearsToAdd)
{
if (this_null) throw new NullPointerException();
int newYear = YEAR.checkValidIntValue(this_year + yearsToAdd);
return { false, newYear, this_month, this_day };
}
Then the result of JIT-compiling the fragment that updates the dates array
might look like:
{ d_null, d_year, d_month, d_day } = dates[0];
dates[0] = plusYears(d_null, d_year, d_month, d_day, 30);
Thanks to the JVM's optimizations, this code never touches a pointer to a
heap-allocated LocalDate object:
-
A flattened reference in
dates[0]is converted to a scalarized reference when read, -
A new scalarized reference is returned from
plusYears, and -
That reference is then converted to another flattened reference when written.
Unlike reference flattening, reference scalarization is not constrained by the
size of the data. Local variables that are pushed and popped on the stack are
not at risk of data races. Thus it is possible to operate routinely on
scalarized encodings of LocalDateTime references: three values and a null flag
for the underlying LocalDate, four values and a null flag for the underlying
LocalTime, and a null flag for the LocalDateTime itself.
JVMs have used similar techniques to scalarize references to identity objects when they can prove that an object's identity is never used. Scalarization of references to value objects is more predictable and far-reaching, even across method boundaries.
When can flattening and scalarization be done?
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.
First, flattening and scalarization require that a variable only stores
references to instances of a specific value class; e.g., the date field of a
LocalDateTime object always stores a LocalDate reference. Flattening and
scalarization typically cannot be applied to a variable declared with a
supertype of a value class, such as Object.
For example, the following two arrays store the same Integer values when they
are created, but because the second array can have arbitrary Object references
stored in it in the future, a JVM must encode its elements as pointers to
ordinary objects on the heap:
Integer[] ints = { 1996, 2006, 1996, null, null }; // flattenable
Object[] objs = { 1996, 2006, 1996, null, null }; // not flattenable
Value objects written to the objs array need to be converted to ordinary heap
objects:
Integer i = -1;
ints[3] = i; // write a flattened reference
objs[3] = i; // write a heap pointer
A field with a generic type T usually has erased type Object, and so behaves
at run time just like an Object-typed field:
record Box<T>(T field) { } // field is not flattenable
var b = new Box<Integer>(i); // field stores a heap pointer
These conversions between encodings do not have any semantic impact — the
Integer objects referenced by objs and field are still value objects, and
do not have identity. The JVM simply encodes the same value object in different
ways.
The same principles apply to method parameters: A parameter of type LocalDate
is reliably scalarizable, while a parameter of type Object or T is not. (If
the method call can be inlined, however, a JIT may be able to skip the
assignment and heap allocation completely.)
A second factor that influences whether a JVM applies flattening and
scalarization is the content of a class file that uses value classes. When a
class is compiled, the names of value classes mentioned by its field and method
signatures are recorded in a new LoadableDescriptors class-file attribute.
This attribute enables the JVM to load the named value classes early enough to
set up flattened fields and scalarized method parameters.
If a value class V is not listed in the LoadableDescriptors attribute of
C.class, then when C is loaded, a JVM may not know that V is a value
class. In practice, this means that if an existing class V is migrated to be a
value class then, for optimal performance, classes that were compiled against
older versions of V should be recompiled.
When reference flattening and scalarization are not possible, a JVM uses an ordinary reference to an object allocated in the heap. In non-optimal circumstances, this may cause a new object to be allocated upon each read of a field or invocation of a method. This behavior is most common during the warmup phase of a program, before a JVM’s JIT has generated optimized code.
Future Work
-
JEP 402, Enhanced Primitive Boxing, will improve the treatment of primitive types to take advantage of the lighter-weight characteristics of boxing to value objects.
-
JEP 218, Generics over Primitive Types (with revisions), will allow generic classes and methods to specialize field, array, and local variable layouts when parameterized by value class types.
Alternatives
-
As discussed, JVMs have long performed escape analysis to identify objects that never rely on identity throughout their lifespan and can be scalarized. These optimizations are somewhat unpredictable, however, and do not help with objects that escape the scope of the optimization, such as storage in fields and arrays.
-
The C language and its relatives support flattened storage for
structs and similar class-like abstractions. For example, the C# language has value types. Unlike value objects, instances of these abstractions have identity, meaning they support operations such as field mutation. As a result, the semantics of copying on assignment, invocation, etc., must be carefully specified, leading to a more complex user model and less flexibility for runtime implementations. We prefer an approach that leaves these low-level details to the discretion of JVM implementors.
Risks and Assumptions
-
This feature makes significant changes to the Java object model. Developers may be surprised by, or encounter bugs due to, changes in the behavior of the
==operator and thesynchronizedkeyword. We expect such disruptions to be rare and tractable. -
Some changes could affect the performance of identity objects. The
if_acmpeqbytecode (==), for example, typically costs only one instruction cycle, but will now need an additional check to detect value objects. But the identity class case can be optimized as a fast path, and we believe we have minimized any performance regressions. -
There is a security risk that the
==operator and theidentityHashCodemethod can indirectly exposeprivatefield values. Further, the==operator can take unbounded time when comparing two large trees of value objects. Developers need to understand these risks.
Dependencies
JEP 539, Strict Field Initialization in the JVM, provides the mechanism to require, via bytecode verification, that value object fields be initialized during the early construction phase.