Records vs Clases: ¿qué me interesa usar?

¿Qué me conviene, un record o una clase? En Java 14 empezaron las previas de los records, una nueva característica del lenguaje de programación Java que te permite crear clases de datos de una manera mucho más concisa que mediante la clase POJO tradicional. Sin embargo, tanto records como clases tienen algunas diferencias y características peculiares así que es importante saber cuál debes usar en cada caso.

Cuando nos adentramos en el mundo de Java, una de las preguntas que surge con frecuencia es qué diferencias existen entre un record y una clase tradicional. Los records son una estructura relativamente nueva en Java que nos permite crear contenedores de información de forma mucho más sencilla y concisa. Por ejemplo, si queremos representar un equipo con un identificador y un nombre, basta con declarar un record con esos dos campos, y automáticamente tendremos un objeto que funciona de manera similar a una clase, con métodos para acceder a sus atributos, además de contar con implementaciones automáticas de toString, equals y hashCode.

En contraste, si creamos una clase tradicional con los mismos atributos, no tendremos nada más que esos campos a menos que escribamos manualmente los métodos para acceder y modificar esos datos. Aunque hoy en día los IDEs nos facilitan mucho esta tarea generando automáticamente getters, setters, constructores y métodos como toString o equals, el código sigue siendo mucho más extenso y verboso que el equivalente en un record. Por ejemplo, una clase que represente lo mismo que un record puede ocupar varias decenas de líneas, mientras que el record apenas unas pocas.

Pero la diferencia más importante no está solo en la cantidad de código, sino en la naturaleza de cada uno. Las clases tradicionales en Java forman parte del sistema completo de orientación a objetos: podemos crear clases abstractas, finales, extender otras clases o implementar interfaces. Además, podemos controlar la visibilidad y mutabilidad de cada atributo, marcándolos como final, public, private, y así sucesivamente.

Los records, en cambio, son inherentemente finales. No podemos extender un record ni crear otro que herede de él. Tampoco podemos modificar sus campos una vez que se ha creado una instancia, ya que todos los atributos de un record son finales. Esto significa que no existen setters ni formas de cambiar el estado interno de un record después de su creación. Por ejemplo, intentar modificar un campo directamente en un record resultará en un error de compilación.

Además, cuando definimos constructores auxiliares en un record, Java nos obliga a llamar al constructor canónico que recibe todos los campos definidos, asegurando que el objeto siempre se cree con todos sus datos completos.

En esencia, un record en Java es el equivalente a lo que en otros lenguajes se conoce como un objeto de transferencia de datos (DTO). Su función principal es encapsular información para transportarla de un módulo a otro de forma sencilla y segura, sin permitir modificaciones posteriores.

Sin embargo, hay situaciones en las que podemos hacer una especie de trampa para introducir mutabilidad dentro de un record. Por ejemplo, si uno de los campos es una colección mutable, como una lista, aunque no podamos cambiar la referencia a esa lista (porque el campo es final), sí podemos modificar el contenido de la lista misma. Esto nos permite, por ejemplo, añadir o eliminar elementos de la lista dentro del record, aunque la referencia a la lista no cambie.

Veamos un ejemplo para ilustrar esto:

import java.util.ArrayList;
import java.util.List;

public record CarritoDeCompra(List<Producto> productos) {

    public CarritoDeCompra() {
        this(new ArrayList<>());
    }

    public int cantidadProductos() {
        return productos.size();
    }

    public double precioTotal() {
        return productos.stream()
                        .mapToDouble(Producto::precio)
                        .sum();
    }
}

record Producto(String nombre, double precio) {}

En este caso, CarritoDeCompra es un record que contiene una lista de productos. Aunque no podemos cambiar la lista productos por otra, sí podemos modificar su contenido usando métodos como add o clear. Esto nos da cierta flexibilidad, pero es importante entender que esta mutabilidad está limitada al contenido de los objetos referenciados, no a los campos del record en sí.

No obstante, esta capacidad tiene sus límites. Si necesitamos campos que sean altamente mutables o que cambien de referencia, los records no son la mejor opción. Por ejemplo, en entornos donde se utilizan frameworks como Hibernate o JPA, que requieren modificar el estado de los objetos para gestionar la persistencia, los records no encajan bien debido a su inmutabilidad inherente.

Por lo tanto, si nuestro objetivo principal es transportar datos de forma sencilla y segura, los records son una herramienta muy útil y elegante. Pero si necesitamos funcionalidades más complejas, como herencia, mutabilidad completa o integración con ciertos frameworks, las clases tradicionales siguen siendo la opción adecuada.

En definitiva, entender estas diferencias nos ayuda a elegir la estructura correcta según las necesidades de nuestro proyecto y a aprovechar las ventajas que cada una ofrece.

Lista de reproducción
  1. 1
    Records de Java: qué son y cómo usarlos
    8 minutos
  2. 2
    ¿Cómo le pongo un setter a un record de Java?
    4 minutos
  3. 3
    Records vs Clases: ¿qué me interesa usar?
    7 minutos
  4. 4
    Pattern matching con records en Java
    9 minutos