Las clases Record declaran implícitamente los campos de componente como final, prohibiendo la mutación después de la construcción. Al emplear un constructor compacto—omitiendo la lista de parámetros formal—el compilador de Java prohíbe la asignación explícita del campo a través de this.component = ... porque automáticamente inyecta el bytecode de asignación inmediatamente después de la ejecución del cuerpo del constructor. Este diseño obliga a los desarrolladores a reasignar las variables de parámetro ellos mismos (por ejemplo, component = Objects.requireNonNull(component)) en lugar de los campos directamente. En consecuencia, la copia defensiva se vuelve esencial para los componentes mutables; dado que el registro almacena referencias, la falta de clonación de argumentos mutables dentro del constructor compacto permite modificaciones externas que violan la garantía de inmutabilidad del registro.
Durante el desarrollo de una plataforma de comercio de alta frecuencia, el equipo de arquitectura adoptó clases Record para representar ticks de datos de mercado inmutables que contienen un precio BigDecimal y una marca de tiempo java.util.Date. La mutabilidad de Date presentó una vulnerabilidad crítica, ya que una condición de competencia podría permitir que un hilo productor modificara el objeto de marca de tiempo después de la instanciación del registro, corrompiendo la auditoría.
Se consideraron tres enfoques para mitigar esta exposición. La primera estrategia consistió en migrar a java.time.Instant, un tipo temporal inmutable. Si bien esto eliminó la sobrecarga de copia defensiva y se alineó con las modernas API de tiempo de Java, requirió una refactorización extensa de los componentes de middleware legados que serializaban objetos Date, introduciendo un riesgo de entrega inaceptable.
La segunda opción utilizó un método de fábrica estático para realizar copia defensiva antes de delegar al constructor canónico. Este enfoque mantuvo la encapsulación pero renunció a la sintaxis concisa y a los beneficios de igualdad estructural automática intrínsecos a los registros, complicando además los marcos de deserialización que esperaban patrones de constructor canónico.
La solución final empleó un constructor compacto para validar entradas y crear clones defensivos: timestamp = (Date) timestamp.clone();. Esto aprovechó la asignación implícita del campo del compilador para almacenar la copia en lugar de la referencia original, asegurando la seguridad de los hilos sin sacrificar la semántica del registro.
La implementación previno con éxito los ataques de manipulación temporal, logrando cero incidentes de corrupción de datos durante las pruebas de estrés posteriores que involucraron millones de transacciones concurrentes.
¿Por qué rechaza el compilador la asignación explícita de this.field dentro de un constructor compacto a pesar de permitirlo en constructores regulares?
La Especificación del Lenguaje Java define los constructores compactos como que se expanden en constructores canónicos donde el compilador sintetiza la lista de parámetros y agrega las asignaciones de campos. Dado que los componentes del registro son implícitamente final, el cuerpo del constructor compacto se ejecuta en un estado de pre-asignación donde los campos se consideran "definitivamente no asignados". Cualquier asignación explícita de this.field constituiría una segunda asignación a una variable final, violando las reglas de asignación definitiva, mientras que la reasignación de la variable de parámetro está permitida, ya que simplemente oculta la asignación implícita que sigue.
¿Cómo protege la copia defensiva en el constructor compacto de un registro contra ataques de deserialización al usar ObjectInputStream?
A diferencia de las clases Serializable tradicionales, que la JVM instancia a través de asignación Unsafe y pobladas mediante reflexión o métodos readObject, los registros deserializados se reconstruyen siempre invocando el constructor canónico con los argumentos suministrados por el flujo. Por lo tanto, la lógica de copia defensiva ejecutada dentro del constructor compacto automáticamente sanea flujos de entrada maliciosos o corruptos que intentan inyectar objetos mutables para modificación posterior. Los desarrolladores a menudo pasan por alto este mecanismo, implementando erróneamente métodos readObject o readResolve en registros donde no son necesarios ni se invocan durante la deserialización estándar.
¿Qué distinción de bytecode existe entre un constructor compacto y un constructor canónico declarado explícitamente en registros?
Un constructor compacto se compila a bytecode donde invokespecial (llamando al constructor de Object) es seguido por la lógica del constructor, luego instrucciones putfield generadas por el compilador para cada componente. Por el contrario, un constructor canónico explícito incrusta operaciones putfield escritas por el desarrollador. Esta distinción impide que los constructores compactos realicen validación o lógica después de la inicialización del campo dentro del mismo método, restringiendo fundamentalmente la secuencia de inicialización y requiriendo que todas las transformaciones defensivas ocurran en las variables de parámetro antes de que las asignaciones implícitas se ejecuten.