JEP 401: Value Objects officially proposed to target JDK 28
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 .
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 Integer and LocalDate , 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.
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 the equals method. 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 the equals method still applies.
It is not a goal to introduce a struct feature, 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.
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.
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.
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.
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.
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.
To use this feature in JDK 28, you must enable preview features:
Compile the program with javac --release 28 --enable-preview Main.java and run it with java --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 featur
Hacker News
news.ycombinator.com