W3docs

Buenas Prácticas de Inmutabilidad en Java

Por qué la inmutabilidad es un buen valor predeterminado en Java y patrones para construir tipos inmutables de forma segura.

Un objeto inmutable es aquel cuyo estado no puede cambiar después de su construcción. Esa única propiedad elimina toda una clase de errores: no hay mutaciones sorpresa desde otro hilo, no hay sorpresas de aliasing donde dos variables comparten estado, y no es necesario hacer copias defensivas en cada lectura. En Java, la inmutabilidad no es automática — hay que construirla deliberadamente. Este capítulo cubre los patrones que hacen que un tipo sea verdaderamente inmutable y los hábitos que lo mantienen así.

Por qué la inmutabilidad es un buen valor predeterminado

El estado mutable compartido es la raíz de la mayoría de los errores de concurrencia y de un número sorprendente de errores en un solo hilo también. Cuando un objeto no puede cambiar, puedes pasarlo libremente, almacenarlo en caché y razonar sobre él sin rastrear quién más tiene una referencia.

PropiedadObjeto mutableObjeto inmutable
Seguridad en hilosNecesita bloqueos o cuidadoInherentemente seguro para hilos
Seguro para compartirNo — los llamadores pueden mutarSí — entrega la misma instancia
Seguro como clave de mapaArriesgado — hashCode puede variarSí — la identidad es estable
Almacenamiento en cachéDebe invalidarse al cambiarCachear indefinidamente
RazonamientoRastrear cada escrituraEl valor se fija en la construcción

El costo es la asignación de memoria: cambiar un campo significa crear un nuevo objeto. Para la mayoría del código ese costo es insignificante y la seguridad lo vale. Recurre a la mutabilidad solo cuando el perfilado demuestre que la necesitas.

Las cinco reglas para una clase inmutable

Una clase es inmutable cuando se cumplen todas las condiciones siguientes. Si falta una, un llamador puede acceder y cambiar el estado.

  1. La clase es final (o todos los constructores son privados) para que no pueda ser extendida con comportamiento mutable.
  2. Todos los campos son private final.
  3. No hay setters — ni ningún otro método que cambie un campo.
  4. Los campos mutables se copian defensivamente al entrar para que la referencia del llamador no pueda usarse para mutar tu estado.
  5. Los getters nunca exponen directamente un objeto interno mutable — devuelven una copia o una vista no modificable.
public final class Money {
    private final long cents;
    private final String currency;

    public Money(long cents, String currency) {
        this.cents = cents;
        this.currency = currency;
    }

    public long cents() { return cents; }
    public String currency() { return currency; }

    // "Change" returns a new object instead of mutating this one.
    public Money plus(Money other) {
        return new Money(this.cents + other.cents, currency);
    }
}

Copias defensivas para campos mutables

Los primitivos y String ya son inmutables, por lo que almacenarlos es seguro. El peligro son los campos mutables — arrays, colecciones, fechas. Si almacenas directamente la referencia del llamador, este conserva un acceso a tus internos.

public final class Schedule {
    private final List<String> slots;

    public Schedule(List<String> slots) {
        // Copy IN: the caller can't mutate our list later.
        this.slots = List.copyOf(slots);
    }

    public List<String> slots() {
        // copyOf already returns an unmodifiable list, so this is safe to hand out.
        return slots;
    }
}

List.copyOf, Set.copyOf y Map.copyOf (Java 10+) realizan ambas tareas a la vez: copian los datos y devuelven una vista no modificable. Para arrays, usa array.clone() al entrar y clone() de nuevo al salir, ya que los arrays siempre son mutables y no tienen un envoltorio de solo lectura.

Records: inmutabilidad por construcción

Un record (Java 16+) es la forma más concisa de declarar un portador inmutable de datos. El compilador genera campos private final, un constructor canónico, accesores y equals/hashCode/toString basados en valor.

public record Point(int x, int y) {
    // Compact constructor for validation and defensive copying.
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("coordinates must be non-negative");
        }
    }
}

Los records cubren el caso común perfectamente, pero no son un escudo mágico: si un componente de un record es un tipo mutable (como List), aún debes copiarlo defensivamente en el constructor compacto, porque el accesor generado devuelve la referencia almacenada tal cual.

Producir copias modificadas: el patrón "wither"

Como no puedes mutar un objeto inmutable, creas una copia modificada. La convención es un método withX que devuelve una nueva instancia con un campo cambiado y el resto conservado.

public final class User {
    private final String name;
    private final String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public User withEmail(String newEmail) {
        return new User(this.name, newEmail); // new object, original untouched
    }
}

Esto mantiene el original seguro para compartir mientras permite a los llamadores construir variaciones. Es el mismo modelo que el JDK usa internamente — LocalDate.plusDays, String.replace y BigDecimal.add todos devuelven nuevas instancias en lugar de mutar el receptor.

Un ejemplo ejecutable completo

El programa a continuación construye una pequeña Account inmutable, luego intenta todos los trucos que un llamador podría usar para mutarla — pasar una lista y mutar el original, mutar la lista devuelta, y renombrar. Demuestra que cada defensa se mantiene, y luego muestra por qué los valores inmutables son claves de mapa seguras.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • Roles after mutating source: [read, write] demuestra que la copia defensiva funcionó — añadir admin a la lista original nunca llegó a la Account.
  • La UnsupportedOperationException en acc.roles().add("hacker") muestra que el getter devolvió una vista no modificable, por lo que los llamadores no pueden mutar los internos a través de ella.
  • Original name still: Ada junto a New object name: Grace demuestra que withName produjo una copia y dejó el original intacto.
  • Different instance: true confirma que el wither devolvió un objeto genuinamente nuevo en lugar de la misma referencia.
  • Records equal by value: true y Key still found: origin-ish muestran que los valores inmutables se comparan y calculan el hash por contenido, lo que los convierte en claves HashMap confiables.

Práctica

Práctica
Cuando una clase inmutable almacena un campo mutable como un List, ¿por qué debes hacer una copia defensiva en el constructor?
Cuando una clase inmutable almacena un campo mutable como un List, ¿por qué debes hacer una copia defensiva en el constructor?
Was this page helpful?