W3docs

Java Records en Profundidad

Un análisis detallado de los records de Java: constructores canónicos y compactos, validación y casos de uso.

Un record es la forma que tiene Java de declarar una clase cuyo único propósito es transportar datos. Introducido como vista previa en Java 14 y finalizado en Java 16, un record elimina el habitual código repetitivo — campos private final, un constructor, accesores, equals, hashCode y toString — en una sola línea de encabezado. El capítulo anterior sobre records mostró la sintaxis básica; este va más a fondo en cómo se comportan realmente los records: sus constructores canónicos y compactos, cómo hacen cumplir invariantes, qué garantías de inmutabilidad ofrecen y dónde encajan (y dónde no).

Lo que el compilador genera por ti

Cuando escribes record Point(int x, int y) {}, el compilador emite una clase final con dos campos private final, un constructor público que acepta ambos, métodos accesores públicos con el mismo nombre que los componentes (x(), y() — sin prefijo get), y métodos equals, hashCode y toString basados en valor.

record Point(int x, int y) {}

// Equivalent to (roughly) hand-writing:
// final class Point {
//   private final int x;
//   private final int y;
//   Point(int x, int y) { this.x = x; this.y = y; }
//   int x() { return x; }
//   int y() { return y; }
//   public boolean equals(Object o) { ... compares x and y ... }
//   public int hashCode() { ... derived from x and y ... }
//   public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }

Los x e y del encabezado son los componentes del record. Los miembros generados por el compilador se derivan completamente de ellos, en el orden de declaración.

Constructores canónicos y compactos

Cada record tiene un constructor canónico cuyos parámetros coinciden con los componentes. Rara vez se escribe completo — en su lugar se usa el constructor compacto, que omite la lista de parámetros y las asignaciones finales this.field = field. El compilador ejecuta tu código primero y luego asigna los parámetros (posiblemente modificados) a los campos. Es el lugar natural para la validación y la normalización.

record Range(int low, int high) {
  Range {                                  // compact constructor — no (int low, int high)
    if (low > high) {
      throw new IllegalArgumentException("low must be <= high");
    }
    low = Math.max(low, 0);                // reassigning the parameter normalizes the field
  }
}

Si alguna vez necesitas la forma canónica explícita (por ejemplo, para copiar defensivamente un componente mutable), escribe la firma completa y haz las asignaciones tú mismo:

record Tags(String name, List<String> values) {
  Tags(String name, List<String> values) {           // explicit canonical constructor
    this.name = name;
    this.values = List.copyOf(values);               // defensive, unmodifiable copy
  }
}

Inmutabilidad y lo que los records no son

Los campos de un record son final, por lo que la referencia que contiene cada componente nunca cambia tras la construcción. Eso hace que los records sean superficialmente inmutables. Pero la inmutabilidad se detiene en la referencia: si un componente apunta a un objeto mutable (como un ArrayList), quienes compartan ese objeto pueden seguir mutando su contenido. Las copias defensivas en el constructor canónico cierran esa brecha.

PropiedadRecordsClases regulares
Campossiempre private finala tu elección
Claseimplícitamente finalextensible salvo que sea final
Superclasesiempre java.lang.Recordcualquiera (por defecto Object)
Accesoresgenerados automáticamente, sin prefijo getescritos a mano
equals/hashCodebasados en valor, generadospor identidad por defecto
Settersninguno — inmutablepermitidos

Como un record siempre extiende java.lang.Record, no puede extender otra clase. Sí puede implementar interfaces, declarar miembros estáticos y añadir métodos de instancia.

Añadir comportamiento, miembros estáticos y fábricas

Un record sigue siendo una clase. Puedes añadirle métodos adicionales, métodos de fábrica estáticos, campos estáticos e incluso tipos anidados. Los componentes definen el estado; todo lo demás es Java ordinario.

record Money(String currency, long cents) {
  static Money of(String currency, long cents) {     // static factory
    return new Money(currency, cents);
  }
  Money plus(Money other) {                          // derived behavior
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("currency mismatch");
    }
    return new Money(currency, cents + other.cents); // returns a new value
  }
}

Los records también se combinan de forma natural con los tipos sellados y el reconocimiento de patrones, modelando conjuntos cerrados de formas de datos — la base del diseño de datos de estilo algebraico en el Java moderno. Una interfaz sellada fija el conjunto de implementaciones de record permitidas, y un switch sobre esos records puede deconstruir cada uno por sus componentes en una sola expresión.

Un ejemplo completo: records de principio a fin

Este programa ejercita los miembros generados de un record, demuestra las propiedades de inmutabilidad y de clase mediante reflexión, hace cumplir un invariante en un constructor compacto, lista los componentes del record en el orden de declaración y muestra los records funcionando con colecciones y comportamiento añadido.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • El Point para el que nunca escribiste un cuerpo imprimió igualmente Point[x=3, y=4], respondió a a.x() e informó equals by value: true con códigos hash coincidentes — el compilador generó toString, accesores, equals y hashCode basados en valor a partir de los dos componentes únicamente.
  • La reflexión confirmó el contrato que garantiza el lenguaje: is final class : true (los records no pueden ser subclasificados) e is a record : true (cada record extiende java.lang.Record), razón por la que no hay setters y los campos son inmutables.
  • La llamada Range(9, 2) fue rechazada con low must be <= high. El constructor compacto se ejecutó antes de que se asignaran los campos, por lo que un record nunca se construye en un estado inválido — la validación pertenece ahí, no en una comprobación de fábrica separada.
  • getRecordComponents() devolvió los componentes en el orden de declaración como low:int high:int, lo que muestra que la estructura de un record es introspectable mediante reflexión — la base para las bibliotecas de serialización y los frameworks que mapean records automáticamente.
  • Money.of("USD", 500).plus(Money.of("USD", 250)) produjo USD 750, y distinct() redujo dos valores idénticos Point(0,0) dejando 2 — los records se comportan como valores propios en todas partes, incluyendo streams y conjuntos, precisamente porque su equals/hashCode compara contenidos.

Cuándo usar un record (y cuándo no)

Opta por un record cuando el tipo está definido por sus datos y esos datos no cambian tras la construcción:

  • DTOs y payloads de solicitud/respuesta de API.
  • Claves de mapas y elementos de conjuntos (equals/hashCode basados en valor vienen de serie).
  • Tipos de retorno que agrupan varios valores, sustituyendo tuplas desechables o parámetros de salida.
  • Las "hojas" de una jerarquía sellada que se desestructura con reconocimiento de patrones.

Prefiere una clase regular cuando:

  • El objeto tiene estado mutable o un ciclo de vida (entidades, constructores, servicios).
  • Necesitas extender otra clase — los records solo pueden implementar interfaces.
  • La identidad del objeto importa más que su contenido (quieres igualdad por referencia).

Un error frecuente: el accesor de un record devuelve la referencia almacenada tal cual. Si un componente es de tipo mutable (un List, un array, un Date), cópialo defensivamente en el constructor canónico — como hace el ejemplo Tags con List.copyOf — de lo contrario, los llamadores pueden mutar el estado "inmutable" del record a través de la referencia que pasaron.

Práctica

Práctica
¿Qué permite hacer el constructor compacto de un record (por ejemplo 'Range { ... }') que un cuerpo de constructor explícito ordinario requeriría más código para lograr?
¿Qué permite hacer el constructor compacto de un record (por ejemplo 'Range { ... }') que un cuerpo de constructor explícito ordinario requeriría más código para lograr?
Was this page helpful?