-
JEP
-
Resolution: Unresolved
-
P3
-
None
-
None
-
Feature
-
Open
-
SE
-
-
XL
-
XL
-
401
Summary
Enhance the Java Platform with value objects, class instances that have
only final
fields and lack object identity.
This is a preview language and VM feature.
Goals
Allow developers to opt in to a programming model for domain values in which objects are distinguished solely by the values of their fields, much as the
int
value3
is distinguished from theint
value4
.Migrate popular classes that represent domain values in the standard library, such as
Integer
andLocalDate
, to this programming model. Support compatible migration of user-defined classes.Maximize the freedom of the JVM to store domain values in ways that improve memory footprint, locality, and garbage collection efficiency.
Non-Goals
It is not a goal to automatically treat existing classes as value classes, even if they meet the requirements for how value classes are declared and used. The behavioral changes require an explicit opt-in.
It is not a goal to "fix" the
==
operator so that programmers can use it in place ofequals
. This JEP redefines==
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 theequals
method still applies.It is not a goal to introduce a
struct
feature in the Java language. Java programmers are not asked to understand new semantics for memory management or variable storage. Java continues to operate on just two kinds of data: primitives and object references.It is not a goal to change the treatment of primitive types. Primitive types behave like value classes in many ways, but are a distinct concept. A separate JEP will provide enhancements to make primitive types more class-like and compatible with generics.
It is not a goal to guarantee any particular optimization strategy or memory layout. This JEP enables many potential optimizations; only some will be implemented initially. Some potential optimizations, such as layouts that exclude null, will only be possible after future language and JVM enhancements.
Motivation
Java developers often need to represent domain values: the date of an event, the
color of a pixel, the shipping address of an order, and so on. Developers
usually model them with immutable classes that contain just enough business
logic to construct, validate, and transform values. Notably, the toString
,
equals
, and hashCode
methods are defined so that equivalent instances can be
used interchangeably.
As an example, event dates can be represented with instances of the <code class="prettyprint" data-shared-secret="1752904268787-0.4805500133635686">LocalDate</code> JDK class:
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
Developers will regard the "essence" of a LocalDate
object as its year, month,
and day values. But to Java, the essence of any object is its identity.
Each time a method in LocalDate
invokes new LocalDate(...)
, an
object with a unique identity is allocated, 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, they are
two objects with distinct identities.
For mutable objects, identity is important: it lets us distinguish two objects
that have the same state now but will have different state in the future. For
example, suppose a class Customer
has a field lastOrderedDate
that is
mutated when the customer makes a new order. Two Customer
objects might have
the same lastOrderedDate
, but it would be a coincidence; when a customer makes
a new order, the application will mutate the lastOrderedDate
of one object but
not the other, relying on identity to pick the right one.
In other words, when objects are mutable, they are not interchangeable. But most
domain values are not mutable and 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 domain value, both
now and in the future. There is no need to distinguish the two objects via their
identities.
In fact, object identity is often harmful if the objects in question have
immutable state and are meant to be interchangeable. This is because it can
cause significant confusion when developers accidentally stumble on it,
discovering that d1 == d3
is false
.
For JDK classes that model primitive values, such as Integer
, the JDK uses a
cache to avoid creating objects with unique identities. However, this cache,
somewhat arbitrarily, does not extend to four-digit Integer
values like
1996
:
jshell> Integer i = 96, j = 96;
i ==> 96
j ==> 96
jshell> i == j
$3 ==> true
jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996
jshell> x == y
$6 ==> false
For domain values like Integer
, the fact that each object has unique identity
is unwanted complexity that leads to surprising behavior and exposes
incidental implementation choices. This extra complexity could be avoided if the
language did not insist that separately-created but interchangeable objects have
distinct identities.
Java's requirement that every object have identity, even if some domain values don't want it, is a performance impediment. Typically, the JVM has to allocate memory for each newly created object, distinguishing it from every object already in the system, and then reference that memory location whenever the object is used or stored.
For example, while an array of int
values can be represented as a simple
block of memory, an array of LocalDate
values must be represented as a
sequence of pointers, each referencing a memory location where an object has
been allocated.
+----------+
| int[5] |
+----------+
| 1996 |
| 2006 |
| 1996 |
| 1 |
| 23 |
+----------+
+--------------+
| 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 modeled by an array of LocalDate
values is not
significantly more complex than an array of int
values (a year-month-day
triple is, effectively, 48 bits of primitive data), the memory footprint is far
greater.
Worse, when a program iterates over the LocalDate
array, each pointer may need
to be dereferenced. CPUs use caches to enable fast access to chunks of memory;
if the array exhibits poor
memory locality
(a distinct possibility if the LocalDate
objects were allocated at different
times or out of order), every dereference may require caching a different
chunk of memory, frustrating performance.
In some application domains, developers routinely program for speed by creating
as few objects as possible, thus de-stressing the garbage collector and
improving locality. For example, they might encode event dates with an int
representing an epoch day. Unfortunately, this approach gives
up the functionality of classes that makes Java code so maintainable:
meaningful names, private state, data validation by constructors, convenience
methods, etc. A developer operating on dates represented as int
values might
accidentally interpret the value in terms of a starting date in
1601 or 1980
rather than the intended 1970 start date.
Trillions of Java objects are created every day, each one bearing a unique
identity. We believe the time has come to let Java developers choose which
objects in the program need identity, and which do not. A class like LocalDate
that represents domain values could opt out of identity, so that it would be
impossible to distinguish between two LocalDate
objects representing the date
1996-01-23
, just as it is impossible to distinguish between two int
values
representing the number 4
.
By opting out of identity, developers are opting in to a programming model that provides the best of both worlds: the abstraction of classes with the simplicity and performance benefits of primitives.
Description
Java programs manipulate objects through references. A reference to an object
is stored in a variable and lets us find the object's fields, which are merely
variables that store primitive values or references to other objects.
Traditionally, a reference also serves as the unique identity of an object: each
execution of new
allocates a fresh object and returns a unique reference that
can be stored in one variable and copied to other variables (aliasing).
Famously, the ==
operator compares objects by comparing references, so
references to two objects are not ==
even if the objects have identical field
values.
JDK NN introduces value objects to model immutable domain values. A reference
to a value object is stored in a variable and lets us find the object's fields,
but it does not serve as the unique identity of the object. Executing new
might not allocate a fresh object and might instead return a reference to an
existing object, or even a "reference" that embodies the object directly. The
==
operator compares value objects by comparing their field values, so
references to two objects are ==
if the objects have identical field values.
A value object is an instance of a value class, declared with the value
modifier. Classes without the value
modifier are called identity classes,
and their instances are identity objects.
Developers can save memory and improve performance by using value objects for
immutable data. Because programs cannot tell the difference between two objects
with identical field values (not even with ==
), the Java Virtual Machine is
able to avoid allocating multiple objects for the same data. Furthermore, the
JVM can change how a value object is laid out in memory without affecting the
program; for example, its fields could be stored on the stack rather than the
heap.
The following sections explore how value objects differ from identity objects and illustrate how to declare value classes. This is followed by an in-depth treatment of the special behaviors of value objects, considerations for value class declarations, and the JVM's handling of value classes and objects.
Enabling preview features
Value classes and objects are a preview language feature, disabled by default.
To try the examples below in JDK NN you must enable preview features:
Compile the program with
javac --release NN --enable-preview Main.java
and 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
.
Programming with value objects
A number of immutable classes in the JDK are declared as value classes. All instances of these classes are value objects. Some popular classes that are made value classes include:
In
java.lang
:Integer
,Long
,Float
,Double
,Byte
,Short
,Boolean
, andCharacter
In
java.util
:Optional
,OptionalInt
,OptionalLong
, andOptionalDouble
In
java.time
:LocalDate
,LocalTime
,Instant
,Duration
,LocalDateTime
,OffsetDateTime
, andZonedDateTime
Objects representing boxed primitives are instances of
Integer
, Long
, etc., and so are always value objects.
For a variety of compatibility and technical reasons, the String
class has not
been made a value class. Strings continue to be identity objects.
In jshell
, the Objects.hasIdentity
method can be used to observe that
String
objects have identity, while Integer
and LocalDate
objects do not.
We can see that the ==
operator compares value objects' field values.
% -> jshell --enable-preview
| Welcome to JShell -- Version 25-internal
| For an introduction type: /help intro
jshell> String s = "abcd", t = "aabcd".substring(1);
s ==> "abcd"
t ==> "abcd"
jshell> Objects.hasIdentity(s)
$12 ==> true
jshell> s == t
$13 ==> false
jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996
jshell> Objects.hasIdentity(x)
$3 ==> false
jshell> x == y
$4 ==> true
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> Objects.hasIdentity(d1)
$8 ==> false
jshell> d1 == d3
$9 ==> true
In most respects, value objects work the way that objects have always worked in Java. However, a few identity-sensitive operations, such as synchronization, are not supported by value objects.
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)
JVMs have a lot of 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, the JVM could implement the array
using "references" that encode the fields of each LocalDate
directly. Each
array component 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 are similar to an
ordinary int
array:
+----------+
| int[5] |
+----------+
| 1996 |
| 2006 |
| 1996 |
| 1 |
| 23 |
+----------+
Some value classes, like LocalDateTime
, are too large to take advantage of
this particular technique. But the lack of identity enables the JVM to optimize
references to those objects in other ways.
Declaring value classes
Developers can declare their own value classes by applying the value
modifier
to any class whose instances can be treated as interchangeable when their field
values match. Records are often great 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)
$7 ==> false
jshell> new Point(17, 3) == p
$8 ==> true
Records are transparent data carriers; but some value classes have
private internal state, and are better expressed with a normal class
declaration. For example, the Substring
value class, below, represents a
string lazily, without allocating a char[]
in memory. The external state of
a Substring
object is the character sequence it represents, while the internal
state includes the original string and a pair of indices.
value class Substring implements CharSequence {
private String str;
private int start, end;
public Substring(String str, int start, int end) {
// (simplification, skipping argument validation)
this.str = str;
this.start = start;
this.end = end;
}
public int length() { return end - start; }
public char charAt(int i) { return str.charAt(start+i); }
public Substring subSequence(int i, int j) {
// (simplification, skipping argument validation)
return new Substring(str, start+i, start+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());
}
}
The instance fields of a value class are always implicitly final
. They may
store references, both to identity objects and to other value objects. In
constructors, the initializer expressions of the fields must not make reference
to this
.
The class itself is also implicitly final
, ensuring that all instances of the
class have the same layout.
Notice that the ==
operator always compares value objects' field values, even
when those fields store private internal state. So two Substring
objects may
represent the same character sequence without being ==
.
jshell> Substring sub1 = new Substring("ionization", 0, 3);
sub1 ==> ion
jshell> Substring sub2 = new Substring("ionization", 7, 10);
sub2 ==> ion
jshell> sub1 == sub2
$3 ==> false
jshell> sub1.equals(sub2)
$4 ==> true
As this example illustrates, the ==
operator does not necessarily align with
intuitions about whether two objects represent the same data. Usually, the
right way to compare objects—whether they have identity or not—is with equals
.
Substitutability
To be more precise, the ==
operator tests whether two objects are
substitutable. This means that identical operations performed on the two
objects will always produce identical results—it is impossible for a program to
distinguish between the two.
For an identity object, this can only be true for the object itself: o1 == o2
only if o1
and o2
have the same unique identity.
For a value object, this is true whenever the two objects are instances of the
same class and have substitutable field values. Primitive field values are
considered substitutable if they have the same bit patterns; reference field
values are compared recursively with ==
.
Here are some examples of ==
comparing value objects with primitive field
values.
jshell> var opt1 = OptionalDouble.of(1.234)
opt1 ==> OptionalDouble[1.234]
jshell> var opt2 = OptionalDouble.of(1.234*1.0)
opt2 ==> OptionalDouble[1.234]
jshell> opt1 == opt2
$3 ==> true
jsahell> opt1.equals(opt2)
$4 ==> true
jshell> Float f1 = Float.intBitsToFloat(0x7ff80000)
f1 ==> NaN
jshell> Float f2 = Float.intBitsToFloat(0x7ff80001)
f2 ==> NaN
jshell> f1 == f2
$7 ==> false
jshell> f1.equals(f2)
$8 ==> true
And here is an example of ==
comparing value objects that store references
to identity objects.
jshell> var opt3 = Optional.of("abcd")
opt3 ==> Optional[abcd]
jshell> var opt4 = Optional.of("aabcd".substring(1))
opt4 ==> Optional[abcd]
jshell> opt3 == opt4
$11 ==> false
jshell> opt3.equals(opt4)
$12 ==> true
When a value object contains a reference to another value object, the ==
operator's recursion performs a "deep" comparison of the nested objects' fields.
The number of comparisons is unbounded: given a deeply-nested stack of
Optional
wrappers, using ==
may require a full traversal of that stack.
jshell> var opt5 = Optional.of(f1)
opt5 ==> Optional[NaN]
jshell> var opt6 = Optional.of(f2)
opt6 ==> Optional[NaN]
jshell> opt5 == opt6
$15 ==> false
jsahell> opt5.equals(opt6)
$16 ==> true
jshell> var opt7 = Optional.of(Optional.of(opt1))
opt7 ==> Optional[Optional[OptionalDouble[1.234]]]
jshell> var opt8 = Optional.of(Optional.of(opt2))
opt8 ==> Optional[Optional[OptionalDouble[1.234]]]
jshell> opt7 == opt8
$19 ==> true
jsahell> opt7.equals(opt8)
$20 ==> true
The requirement that the fields of a value class be initialized without
reference to this
ensures that the recursive application of ==
to referenced
value objects will never cause an infinite loop.
Identity-sensitive operations
In most respects, a value object is indistiguishable from an identity object. But the following operations on objects behave differently when the object does not have identity:
The
==
and!=
operators, as described above, test whether two objects are substitutable, which for value objects means comparing their internal state.A user of a value object should never assume unique ownership of that object, because a substitutable instance might be created by someone else.
A new
Objects.hasIdentity
method returnsfalse
for value objects.Objects.requireIdentity
is also available, throwing anIdentityException
when given a value object.The
System.identityHashCode
method hashes together a value object's class and its field values, ensuring that the same hash code is returned for equivalent value objects.Value objects cannot be used for synchronization. Attempts to synchronize on a value class type are rejected at compile time; attempts to synchronize on a value object typed as a supertype (like
Object
) will fail at run time with anIdentityException
.Similarly, the usual understandings of object lifespan and garbage collection do not apply to value objects, because a substitutable instance may be recreated at any point. So
javac
producesidentity
warnings about uses of thejava.lang.ref
API at compile time, and the library throws anIdentityException
at run time.
In anticipation of these new behaviors, the value classes in the standard library have long been marked as value-based, warning that users should not depend on the unique identities of instances. Programs that have followed this advice, avoiding identity-sensitive operations on these objects, can expect consistent behavior between releases.
Applying or removing the value
keyword is a binary-compatible change. While
the behavior of identity-sensitive operations will change, classes will continue
to link.
The java.lang.Object
class
Every value class is a subclass of java.lang.Object
, just like every identity
class.
The Object.equals
method performs a ==
test by default, comparing the
internal state of value objects. It will often be necessary for the author of a
value class to override equals
in order to properly compare the objects'
external state.
In a value record, as for all records, the implicit equals
recursively applies
equals
to the record components. This behavior does not always match the
behavior of ==
—for example, if a value record has a String
component, ==
compares the strings' identities, while equals
compares their contents.
Similarly, Object.hashCode
delegates to System.identityHashCode
by default,
which produces a hash for a value object's internal state. As usual, the
hashCode
method should be overridden by a value class whenever it overrides
equals
.
The default toString
has the usual form, "ClassName@hashCode"
, but note that
the default hashCode
of a value object is derived from its internal state, not
object identity. Most value classes will want to override toString
to more
legibly convey the domain value represented by the object.
A few other methods of Object
interact with value objects in interesting ways:
For a
Cloneable
value class, theObject.clone
method produces a value object that is indistinguishable from the original—the usual expectation thatx.clone() != x
is not meaningful for value objects. Value classes that store references to identity objects may wish to overrideclone
and perform a "deep copy" of these identity objects.The
wait
andnotify
methods 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 will always fail with anIllegalMonitorStateException
.The
finalize
method of a value object will never be invoked by the garbage collector.
It has always been possible to create plain instances of java.lang.Object
with
new Object()
. This behavior is still supported, and direct instances of the
Object
class are always identity objects.
Extending classes and implementing interfaces
A value class can implement any interface. When a variable has an interface type, that variable may store a value object or an identity object.
A value class cannot extend a concrete class (with the special exception of
Object
).
Abstract classes may be value classes or identity classes. The value
modifier
applied to an abstract class indicates that the class has no need for identity,
but does not restrict its subclasses. Just like an interface, an abstract value
class may be extended by both identity classes and value classes. An abstract
class without the value
modifier is an identity class, and may only be
extended by other identity classes.
Thus, a value class can extend an abstract value class, but cannot extend an abstract identity class.
Many abstract classes are good value class candidates. The class Number
, for
example, has no fields, nor any code that depends on identity-sensitive
features.
abstract value class Number implements Serializable {
public abstract int intValue();
public abstract long longValue();
public byte byteValue() { return (byte) intValue(); }
...
}
All of the usual rules for value classes apply to abstract value classes—for
example, any instance fields of an abstract value class are implicitly final
.
Safe construction
Constructors initialize newly-created objects, including setting the values of the objects' fields. Because value objects do not have identity, their initialization requires special care.
An object being constructed is "larval"—it has been created, but it is not yet fully-formed. Larval objects must be handled carefully, because the expected properties and invariants of the object may not yet hold—for example, the fields of a larval object may not be set. If a larval object is shared with outside code, that code may even observe the mutation of a final field!
Traditionally, a constructor begins the initialization process by invoking a
superclass constructor, super(...)
. After the superclass returns, the subclass
then proceeds to set its declared instance fields and perform other
initialization tasks. This pattern exposes a completely uninitialized subclass
to any larval object leakage occurring in a superclass constructor.
The Flexible Constructor Bodies feature enables an
alternative approach to initialization, in which fields can be set and other
code executed before the super(...)
invocation. There is a two-phase
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 phase are set before they can ever be read, even if a superclass leaks the
larval object. Final fields, in particular, can never be observed to mutate.
In a value class, all constructor and initializer code normally occurs in the
early construction phase. This means that attempts to invoke instance methods or
otherwise use this
will fail:
value class Name {
String name;
int length;
Name(String n) {
name = n;
length = computeLength(); // error!
}
private int computeLength() {
return name.length();
}
}
Field that are declared with initializers get set at the start of the
constructor (as usual), but any implicit super()
call gets placed at the end
of the constructor body.
When a constructor includes code that needs to work with this
, an explicit
super(...)
or this(...)
call can be used to mark the transition to the late
phase. But all fields must be initialized before the super(...)
call, without
reference to this
:
value class Name {
String name;
int length;
Name(String n) {
name = n;
length = computeLength(name); // ok
super(); // all fields must be set at this point
System.out.println("Name: " + this);
}
// refactored to be static:
private static int computeLength(String n) {
return n.length();
}
}
For convenience, the early construction rules are relaxed by this JEP to allow
the class's fields to be read as well as written—both references to the
field name
in the above constructor are legal. It continues to be illegal to
refer to inherited fields, invoke instance methods, or share this
with other
code until the late construction phase.
Instance initializer blocks (a rarely-used feature) continue to run in the late phase, and so may not assign to value class instance fields.
This scheme is also appropriate for identity records, so this JEP modifies the
language rules for records such that their constructors always run in the early
construction phase. When access to this
is needed, an explicit super()
can
be inserted, but the record's fields must be set beforehand. The following
record declaration will fail to compile when preview features are enabled,
because it now makes reference to this
in the early construction phase.
record Node(String label, List<Node> edges) {
public Node {
validateNonNull(this, label); // error!
validateNonNull(this, edges); // error!
}
static void validateNonNull(Object o, Object val) {
if (val == null) {
throw new IllegalArgumentException(
"null arg for " + o);
}
}
}
(Note that this attempt to provide useful diagnostics by sharing this
is
misguided anyway: in a record's compact constructor, the fields are not set
until the end of the constructor body; before they are set, the toString
result will always be Node[label=null, edges=null]
.)
Finally, in normal identity classes, we think developers should write
constructors and initializers that avoid the risk of larval object leakage by
generally adopting the early construction constraints: read and write the
declared fields of the class, but otherwise avoid any dependency on this
, and
where a dependency is necessary, mark it as deliberate by putting it after an
explicit super(...)
or this(...)
call. To encourage this style, javac
provides lint
warnings indicating this
dependencies in normal identity
class constructors. (In the future, we anticipate that normal identity classes
will have a way to adopt the constructor timing of value classes and records. A
class that compiles without warning will likely be able to cleanly make that
transition.)
When to declare a value class
As we've illustrated, value classes and value records are a useful tool for modeling immutable domain values that are interchangeable when they have matching internal state.
As a general rule, if a class doesn't need identity, it should probably be a value class. This is especially true for abstract classes, which often have no need for identity—they may have no state at all—and shouldn't impose an identity requirement on their subclasses.
But before applying the value
keyword, class authors should take some
additional considerations into account:
Compatibility. Developers who maintain published identity classes should
decide whether any users are likely to depend on identity. For example, these
classes should have overridden equals
and hashCode
so that these methods'
behavior is not identity-sensitive.
Classes with public
constructors are particularly at risk, because in the past
users could count on the new
operation producing a unique reference that the
user "owned". Subsequent uses of ==
or synchronization may depend on that
assumption of unique ownership. (Most of the value classes in the JDK have
avoided this obligation by allowing object creation only through factory
methods.)
Serialization. Some classes that model domain values are likely to implement
Serializable
. Traditional object deserialization in the JDK does not safely
initialize the class's fields, and so is incompatible with value classes.
Attempts to serialize or deserialize a value object will generally fail unless
it is a value record or a JDK class instance.
Value class authors can work around this limitation with a serialization proxy,
using the writeReplace
and readResolve
methods. (A value record may be a
good candidate for a proxy class.) In the future, enhancements to the
serialization mechanism are anticipated that will allow value classes to be
serialized and deserialized directly.
Subtyping. A value class must either be abstract
or final
. Concrete
classes that have their own value object instances, while also being extended by
other classes, are not supported. If a type hierarchy is needed, the supertypes
in the hierarchy must be abstract value classes or interfaces.
Sensitive state. Because the ==
operator and identityHashCode
depend on
a value object's internal state, a malicious user could use those operations to
try to infer the internal state of an object. Value classes are not designed to
protect sensitive data against such attacks.
Complexity. Most domain values are simple data aggregates. Value classes are
not designed for managing large recursive data structures or unusually large
numbers of fields. Users who apply the value
keyword to such complex classes
may experience slow ==
operations or other performance regressions when
compared to identity classes.
Run-time optimizations for value objects
At run time, JVMs will typically optimize the use of value object references by avoiding traditional heap object allocation as much as possible, preferring reference encodings that refer to data stored on the stack, or that embed the data in the reference itself.
As we saw earlier, an array of LocalDate
references might be flattened so
that the array stores the objects' data directly. (The details of flattened
encodings will vary, of course, at the discretion of the JVM implementation.)
+--------------+
| LocalDate[5] |
+--------------+
| 1|1996|01|23 |
| 1|1996|01|23 |
| 1|2026|01|23 |
| 0|0000|00|00 |
| 1|1996|01|23 |
+--------------+
An array of boxed Integer
objects can be similarly flattened, in this case by
simply concatenating a null flag to each int
value.
+--------------+
| Integer[5] |
+--------------+
| 1|1996 |
| 1|2006 |
| 1|1996 |
| 1|1 |
| 1|23 |
+--------------+
The layout of this array is not significantly different from that of a plain
int
array, except that it requires some extra bits for each null flag (in
practice, this probably means that each reference takes up 64 bits).
Flattening can be applied to fields as well—a LocalDateTime
on the heap could
store flattened LocalDate
and LocalTime
references directly in its object
layout.
+----------------------+
| LocalDateTime |
+----------------------+
| date=1|2026|01|23 |
| time=1|09|00|00|0000 |
+----------------------+
Heap flattening must maintain the integrity of object data. For example, the
flattened reference must be read and written atomically, or it could become
corrupted. On common platforms, this limits the size of most flattened
references to no more than 64 bits. So while it would theoretically be possible
to flatten LocalDateTime
references too, in practice they would probably be
too big. In the future, 128-bit flattened encodings may be used on platforms
that support atomic reads and writes of that size. And the
Null-Restricted Value Types JEP
will enable heap flattening for even larger value classes if the programmer is
willing to opt out of atomicity guarantees.
When these flattened references are read from heap storage, they need to be re-encoded in a form that the JVM can readily work with. One strategy is to store each field of the flattened reference in a separate local variable. This set of local variables constitutes a scalarized encoding of the value object reference.
In HotSpot, scalarization is a JIT compilation technique, affecting the representation of references to value objects in the bodies and signatures of JIT-compiled methods.
The following code reads a LocalDate
from an array and invokes the plusYears
method. The simplified contents of the plusYears
method is included for
reference.
LocalDate d = arr[0];
arr[0] = d.plusYears(30);
public LocalDate plusYears(long yearsToAdd) {
// avoid overflow:
int newYear = YEAR.checkValidIntValue(this.year + yearsToAdd);
// (simplification, skipping leap year adjustment)
return new LocalDate(newYear, this.month, this.day);
}
In pseudo-code, the JIT-compiled code might look like the following, where the
notation { ... }
refers to a vector of multiple values. (Importantly, this is
purely notational: there is no wrapper at run time.)
{ d_null, d_year, d_month, d_day } = $decode(arr[0]);
arr[0] = $encode($plusYears(d_null, d_year, d_month, d_day, 30));
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 };
}
Notice that this code never interacts with a pointer to a heap-allocated
LocalDate
—a flattened reference is converted to a scalarized reference, a
new scalarized reference is created, and then that reference is converted to
another flattened reference.
Unlike heap flattening, scalarization is not constrained by the size of the
data—local variables being operated on in the stack are not at risk of data
races. A scalarized encoding of a LocalDateTime
reference might consist of a
null flag, four components for the LocalDate
reference, and five components
for the LocalTime
reference.
JVMs have used similar techniques to scalarize identity objects in local code when the JVM is able to prove that an object's identity is never used. But scalarization of value objects is more predictable and far-reaching, even across non-inlinable method invocation boundaries.
One limitation of both heap flattening and scalarization is that it is not
typically applied to a variable with a type that is a supertype of a value
class type. Notably, this includes method parameters of generic code whose
erased type is Object
. Instead, when an assignment to a supertype occurs, a
scalarized value object reference may be converted to an ordinary heap object
reference. But this allocation occurs only when necessary, and as late as
possible.
Class file changes
This JEP includes two preview VM features to support value classes:
The
ACC_IDENTITY
flag indicates that a class is an identity class. It is left unset for value classes and interfaces. This flag replaces theACC_SUPER
flag, which has been unused by the JVM since Java 8.The
LoadableDescriptors
attribute lists the names of value classes appearing in the field or method descriptors of the current class. This attribute authorizes the JVM to load the named value classes early enough that it can optimize the layouts of references to instances from the current class.
Additionally, compiled value classes use the features of Strict Field Initialization in the JVM (Preview) to guarantee that the class's fields are properly initialized.
Library changes
The full list of classes in the JDK that are treated as value classes when preview features are enabled is as follows:
In
java.lang
:Integer
,Long
,Float
,Double
,Byte
,Short
,Boolean
,Character
,Number
, andRecord
In
java.util
:Optional
,OptionalInt
,OptionalLong
, andOptionalDouble
In
java.time
:LocalDate
,Period
,Year
,YearMonth
,MonthDay
,LocalTime
,Instant
,Duration
,LocalDateTime
,OffsetTime
,OffsetDateTime
,ZonedDateTime
In
java.time.chrono
:HijrahDate
,JapaneseDate
,MinguoDate
,ThaiBuddhistDate
, andChronoLocalDateImpl
The methods Objects.hasIdentity
and Objects.requireIdentity
, and the
IdentityException
class, are (reflective?) preview APIs.
The serialization and java.lang.ref
APIs require special handling for value
objects.
Future Work
Null-Restricted Value Class Types (Preview) will build on this JEP, allowing programmers to manage the storage of nulls and enable more dense heap flattening in fields and arrays.
Enhanced Primitive Boxing (Preview) will enhance the language's use of primitive types, taking advantage of the lighter-weight characteristics of boxing to value objects.
JVM class and method specialization (JEP 218, 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, and do not help with objects that escape the scope of the optimization, including storage in fields and arrays.
Hand-coded optimizations via primitive values are possible to improve performance, but as noted in the "Motivation" section, these techniques require giving up valuable abstractions.
The C language and its relatives support flattened storage for struct
s 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
implementations.
Risks and Assumptions
The feature makes significant changes to the Java object model. Developers may
be surprised by, or encounter bugs due to, changes in the behavior of operations
such as ==
and synchronized
. We expect such disruptions to be rare and
tractable.
Some changes could potentially affect the performance of identity objects. The
if_acmpeq
test, for example, typically only costs 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 ==
and hashCode
can indirectly expose
private
field values. Further, two large trees of value objects can take
unbounded time to compute ==
. Developers need to understand these risks.
Dependencies
In anticipation of this feature we already added warnings about potential
behavioral incompatibilities for value class candidates in javac
and
HotSpot, via Warnings for Value-Based Classes and
JDK-8354556
Flexible Constructor Bodies allows
constructors to execute statements before a super(...)
call and allows
assignments to instance fields in this context. These changes facilitate the
construction protocol required by value classes.
Strict Field Initialization in the JVM (Preview) provides the JVM mechanism necessary to require, through verification, that value class instance fields are initialized during early construction
- relates to
-
JDK-8277163 Value Objects (Preview)
-
- Closed
-
-
JDK-8354832 Shallow equality of value objects
-
- New
-