Jorge Turbica / blog · Blog ← Todos los artículos EN · Read in English
Lenguajes

JDK 28 en preview quita la identidad a Integer y LocalDate: == cambia sin tocar tu código

JEP 401 convierte 30 clases del JDK en value classes con --enable-preview: ==, synchronized y WeakHashMap cambian. Cómo probarlo en CI.

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:

PaqueteClases
java.langInteger, Long, Float, Double, Byte, Short, Character, Boolean, Number, Record
java.utilOptional, OptionalInt, OptionalLong, OptionalDouble
java.timeLocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetTime, OffsetDateTime, Duration, Instant, Period, Year, YearMonth, MonthDay
java.time.chronoMinguoDate, 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 devolver true donde antes devolvía false. Si algún test o alguna caché dependían de que dos LocalDate iguales fueran objetos distintos, dejarán de cumplirse.
  • Sin monitor. No se puede sincronizar sobre un value object, y wait o notify no están soportados: la interacción con el monitor da un error de compilación o una IdentityException en tiempo de ejecución. Un synchronized (someInteger) es el caso típico.
  • Sin referencias débiles. Las APIs de java.lang.ref y java.util.WeakHashMap lanzan IdentityException con value objects. Una caché basada en WeakHashMap<Long, …> deja de funcionar.
  • Inmutables de verdad. setAccessible sobre sus campos acaba en IllegalAccessException, aunque esté habilitada la mutación de campos final. 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:

  1. Quitar la sentencia problemática.
  2. Reescribirla para que funcione en un contexto de early construction, que se parece mucho a uno estático.
  3. Asignar todos los campos, llamar a super() o this(...), 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.

java
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:

xml
<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 synchronized sobre wrappers, Optional o tipos de java.time, y WeakHashMap o WeakReference con esas claves.
  • Revisa los == entre objetos de esas clases: con la preview activa pueden empezar a dar true.
  • Comprueba que tus librerías de serialización, mocking o inyección no hacen setAccessible sobre 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

Jorge Turbica

Software Engineer y SRE. Escribo sobre los retos reales que me encuentro en producción.

Ver portfolio ↗