Valhalla llega a JDK 28: value objects en preview
El JEP 401 ya está integrado en JDK 28. Con --enable-preview, == compara por valor y 30 clases de la plataforma, como Integer, Optional o LocalDate, dejan de tener identidad.
El JEP 401, Value Objects, está integrado en JDK 28 como preview. Es la primera pieza de Project Valhalla que entra en el JDK, y toca algo que usas cada día sin pensarlo: qué significa == entre dos objetos.
El JEP se creó en agosto de 2020. Seis años después, ya puedes probarlo en las builds de acceso anticipado de JDK 28.
Es una preview: hace falta --enable-preview para compilar y para ejecutar, y puede cambiar antes de ser definitiva. No es para producción.
Objetos sin identidad
Un value object es inmutable y no tiene identidad: se distingue solo por el valor de sus campos. Lo declaras con el modificador value, también en un record:
// javac --release 28 --enable-preview Main.java
// java --enable-preview Main
value record Point(int x, int y) {}
void main() {
var a = new Point(1, 2);
var b = new Point(1, 2);
IO.println(a == b); // true: compara los campos, no la identidad
}Según el JEP, dos value objects son == si son de la misma clase, sus campos primitivos guardan los mismos bits y sus campos de referencia son, a su vez, == aplicando la misma regla. Ojo con ese último punto: si tu value class tiene un String, ese campo se sigue comparando por referencia. El JEP dice expresamente que no pretende que == sustituya a equals, así que para comparar contenido sigue usando equals.
Integer, por fin coherente
El ejemplo del JEP es de los que se cuentan en el café. Hoy, sin preview:
jshell> Integer x = 1996, y = 1996;
x ==> 1996
y ==> 1996
jshell> x == y
$6 ==> falseCon 1 sale true y con 1996 sale false, porque Integer usa una caché para los valores pequeños. Arrancando jshell --enable-preview en JDK 28, el mismo x == y devuelve true: dos Integer son == si representan el mismo valor.
Con la preview activada, 30 clases de la plataforma pasan a ser value classes:
java.lang:Integer,Long,Float,Double,Byte,Short,Character,Boolean,NumberyRecord.java.util:Optional,OptionalInt,OptionalLongyOptionalDouble.java.time:LocalDate,LocalTime,LocalDateTime,ZonedDateTime,OffsetTime,OffsetDateTime,Duration,Instant,Period,Year,YearMonthyMonthDay.java.time.chrono:MinguoDate,HijrahDate,JapaneseDateyThaiBuddhistDate.
Lo que ya no puedes hacer
Las reglas son pocas y estrictas. Todos los campos de instancia son implícitamente final. La clase también lo es, salvo que la declares abstract, y una value class solo puede extender Object o una value class abstracta.
Y lo que depende de la identidad deja de funcionar. El ejemplo del JEP con un LocalDate:
jshell> synchronized (d1) { d1.notify(); }
| Error:
| unexpected type
| required: a type with identity
| found: java.time.LocalDate
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.LocalDateSi en algún rincón de tu código se sincroniza sobre un Integer o una fecha, es buen momento para buscarlo: el JEP avisa de que ese código falla tras la migración, en compilación o con IdentityException en ejecución.
Y el rendimiento
La gracia de quitar la identidad es que la JVM gana libertad para representar estos objetos como le convenga. El JEP pone un ejemplo: cada elemento de un array de LocalDate podría ocupar una palabra de 64 bits que indica si la referencia es null y, si no, guarda directamente el año, el mes y el día. Las referencias a value objects siguen pudiendo ser null.
El JEP no da cifras de rendimiento, así que nosotros tampoco.
JEP 539, la pieza de la JVM
Junto al 401 se ha integrado el JEP 539, Strict Field Initialization in the JVM, también en preview. Añade a la JVM campos que deben inicializarse antes de leerse, de modo que nunca se observa un 0 o un null por defecto. Los compiladores marcan así todos los campos de una value class. No es una feature del lenguaje ni cambia cómo javac compila tu código actual: no tienes que hacer nada.
Cómo probarlo
Descarga una build de acceso anticipado de JDK 28 y abre jshell --enable-preview, o compila con javac --release 28 --enable-preview. Nuestro consejo: coge un par de records inmutables de tu proyecto, ponles value en una rama y mira qué se rompe.