Treinta clases del JDK, entre ellas Integer, Double, Optional y LocalDate, pierden su identidad en JDK 28 en cuanto se arranca la JVM con --enable-preview. Es la consecuencia más visible de JEP 401, que introduce las value classes de Valhalla como función en preview, y lo inusual es que afecta a código que no ha cambiado ni una línea. El 27 de octubre de 2026 el OpenJDK Quality Group pidió a los proyectos open source que prueben ya contra JDK 28 con y sin preview.
Qué trae JEP 401 y por qué toca tu código sin que lo uses
Una value class es una clase cuyas instancias no tienen identidad: dos objetos con los mismos campos son indistinguibles. Ya no importa cuál de los dos tienes, solo qué contiene.
Normalmente una función en preview solo afecta a quien escribe código nuevo con ella. Aquí no: con la preview activada, el propio JDK declara como value classes estas 30 clases, y las usa todo el mundo:
| Paquete | Clases |
|---|---|
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 |
El cambio de comportamiento se aplica al ejecutar en JDK 28 con la preview activa, aunque el código se compilara sin ella o con una versión anterior de Java. Un JAR de hace años puede comportarse distinto solo por el flag con el que arrancas la JVM. Sin --enable-preview, que es lo predeterminado, el comportamiento debería seguir siendo el de siempre.
Lo que se rompe: cuatro operaciones que dependen de la identidad
La lista es corta, pero cada punto es un patrón que aparece en bases de código reales:
==compara por valor. Con instancias de estas clases,==puede devolvertruedonde antes devolvíafalse. Si algún test o alguna caché dependían de que dosLocalDateiguales fueran objetos distintos, dejarán de cumplirse.- Sin monitor. No se puede sincronizar sobre un value object, y
waitonotifyno están soportados: la interacción con el monitor da un error de compilación o unaIdentityExceptionen tiempo de ejecución. Unsynchronized (someInteger)es el caso típico. - Sin referencias débiles. Las APIs de
java.lang.refyjava.util.WeakHashMaplanzanIdentityExceptioncon value objects. Una caché basada enWeakHashMap<Long, …>deja de funcionar. - Inmutables de verdad.
setAccessiblesobre sus campos acaba enIllegalAccessException, aunque esté habilitada la mutación de camposfinal. Afecta a librerías de serialización o de mocking que manipulan estos objetos por reflexión.
Nada de esto llega por sorpresa. JDK 8 ya definió las value-based classes, con Optional y java.time, y advirtió de que no había que depender de su identidad; JDK 16 amplió esa categoría a los wrappers de primitivos. Lo que JEP 401 hace es convertir aquella advertencia de la documentación en comportamiento de la JVM. Quien hizo caso entonces apenas notará nada; quien sincronizaba sobre un Integer lo va a notar de inmediato.
Los records también cambian al compilar con preview
El segundo frente afecta a la compilación. Si compilas con JDK 28 y la preview activada, el código de los constructores de records se ejecuta por defecto en la fase de early construction que formalizó JEP 513 (cuerpos de constructor flexibles). En esa fase solo se puede asignar campos, así que cualquier sentencia que use this, por ejemplo llamar a un método de instancia, pasa a ser un error de compilación. Los constructores ya compilados sin preview siguen ejecutándose tal y como se compilaron.
Hay tres salidas, según el caso:
- Quitar la sentencia problemática.
- Reescribirla para que funcione en un contexto de early construction, que se parece mucho a uno estático.
- Asignar todos los campos, llamar a
super()othis(...), que cierra esa fase, y ejecutar después lo que antes daba error.
Los constructores compactos no deberían contener nada así, porque exponen la instancia con todos los campos a null o a 0. Si lo hacen y la sentencia no se puede eliminar, hay que pasarlos a la forma no compacta.
record Range(int from, int to) {
Range {
validate(); // error en JDK 28 con preview: usa `this` antes de tiempo
}
void validate() {
if (from > to) throw new IllegalArgumentException();
}
}En este ejemplo basta con mover la comprobación a una condición sobre los parámetros (if (from > to) …) en lugar de llamar a un método de instancia.
Cómo probarlo en tu CI
La propuesta de OpenJDK es lanzar la suite dos veces contra JDK 28: con la configuración normal, para detectar regresiones que no deberían existir, y con la preview activa, para ver qué dejaría de funcionar cuando las value classes sean definitivas. Con Maven, el perfil de preview necesita el flag en la compilación y en los tests:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>28</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>En Gradle, el equivalente es añadir --enable-preview a options.compilerArgs de las tareas JavaCompile y a jvmArgs de las tareas Test.
Para validar el resultado:
- Busca
synchronizedsobre wrappers,Optionalo tipos dejava.time, yWeakHashMapoWeakReferencecon esas claves. - Revisa los
==entre objetos de esas clases: con la preview activa pueden empezar a dartrue. - Comprueba que tus librerías de serialización, mocking o inyección no hacen
setAccessiblesobre sus campos. - Recompila con preview y corrige los constructores de records que fallen.
- Si encuentras un fallo que no encaja en lo anterior, es un bug del JDK y OpenJDK lo quiere en la lista valhalla-dev.
Qué significa
Pocas previews del JDK cambian el comportamiento de código que nadie ha tocado; esta lo hace con tipos que aparecen en casi cualquier aplicación Java. Al ser una preview hay margen: nada cambia en producción mientras no actives el flag. Pero el momento de descubrir un synchronized sobre un Integer es ahora, en un job de CI aparte, y no cuando la función deje de ser preview.
Fuentes
- Quality Outreach Heads-up - JDK 28: Value Classes, Inside Java, 27 de octubre de 2026.