W3docs

Best Practice Java sull'Immutabilità

Perché l'immutabilità è un buon default in Java e i pattern per costruire tipi immutabili in modo sicuro.

Un oggetto immutabile è un oggetto il cui stato non può cambiare dopo la costruzione. Questa singola proprietà elimina un'intera classe di bug: non ci sono mutazioni a sorpresa da un altro thread, nessuna sorpresa di aliasing in cui due variabili condividono lo stato, e nessuna necessità di copie difensive ad ogni lettura. In Java, l'immutabilità non è automatica — bisogna costruirla deliberatamente. Questo capitolo tratta i pattern che rendono un tipo veramente immutabile e le abitudini che lo mantengono tale.

Perché l'immutabilità è un buon default

Lo stato mutabile condiviso è la radice della maggior parte dei bug di concorrenza e di un numero sorprendente di quelli single-thread. Quando un oggetto non può cambiare, puoi passarlo liberamente, metterlo in cache e ragionare su di esso senza dover tracciare chi else tiene un riferimento.

ProprietàOggetto mutabileOggetto immutabile
Thread safetyRichiede lock o attenzioneIntrinsecamente thread-safe
Sicuro da condividereNo — i chiamanti possono mutareSì — distribuisci la stessa istanza
Sicuro come chiave di mappaRischioso — hashCode può variareSì — l'identità è stabile
CachingBisogna invalidare al cambiamentoCacheable indefinitamente
RagionamentoTraccia ogni scritturaIl valore è fisso alla costruzione

Il costo è l'allocazione: cambiare un campo significa creare un nuovo oggetto. Per la maggior parte del codice questo costo è trascurabile e la sicurezza ne vale la pena. Ricorri alla mutabilità solo quando il profiling dimostra che ne hai bisogno.

Le cinque regole per una classe immutabile

Una classe è immutabile quando tutte le seguenti condizioni sono soddisfatte. Mancarne una e un chiamante potrà intervenire e modificare lo stato.

  1. La classe è final (o tutti i costruttori sono privati) in modo che non possa essere sottoclassata con comportamento mutabile.
  2. Tutti i campi sono private final.
  3. Non ci sono setter — né altri metodi che modificano un campo.
  4. I campi mutabili sono copiati difensivamente in ingresso affinché il riferimento del chiamante non possa essere usato per mutare il tuo stato.
  5. I getter non espongono mai direttamente un oggetto interno mutabile — restituiscono una copia o una vista non modificabile.
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);
    }
}

Copie difensive per i campi mutabili

I primitivi e String sono già immutabili, quindi memorizzarli è sicuro. Il pericolo sono i campi mutabili — array, collezioni, date. Se memorizzi direttamente il riferimento del chiamante, questi mantiene un accesso alle tue strutture interne.

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 e Map.copyOf (Java 10+) svolgono entrambi i compiti contemporaneamente: copiano i dati e restituiscono una vista non modificabile. Per gli array, usa array.clone() in ingresso e clone() nuovamente in uscita, poiché gli array sono sempre mutabili e non hanno un wrapper di sola lettura.

Record: immutabilità per costruzione

Un record (Java 16+) è il modo più conciso per dichiarare un portatore immutabile di dati. Il compilatore genera campi private final, un costruttore canonico, accessor e equals/hashCode/toString basati sul valore.

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");
        }
    }
}

I record coprono il caso comune in modo eccellente, ma non sono uno scudo magico: se un componente di un record è di tipo mutabile (come List), devi comunque copiarlo difensivamente nel costruttore compatto, perché l'accessor generato restituisce il riferimento memorizzato così com'è.

Produrre copie modificate: il pattern "wither"

Poiché non puoi mutare un oggetto immutabile, crei una copia modificata. La convenzione è un metodo withX che restituisce una nuova istanza con un campo cambiato e il resto copiato.

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
    }
}

Questo mantiene l'originale sicuro da condividere mentre permette ai chiamanti di costruire varianti. È lo stesso modello che il JDK usa internamente — LocalDate.plusDays, String.replace e BigDecimal.add restituiscono tutti nuove istanze invece di mutare il ricevente.

Un esempio completo eseguibile

Il programma seguente costruisce un piccolo Account immutabile, poi tenta ogni trucco che un chiamante potrebbe usare per mutarlo — passando una lista e mutando l'originale, mutando la lista restituita e rinominando. Dimostra che ogni difesa regge, poi mostra perché i valori immutabili sono chiavi di mappa sicure.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Roles after mutating source: [read, write] dimostra che la copia difensiva ha funzionato — aggiungere admin alla lista originale non ha mai raggiunto l'Account.
  • L'UnsupportedOperationException su acc.roles().add("hacker") mostra che il getter ha restituito una vista non modificabile, quindi i chiamanti non possono mutare gli interni attraverso di esso.
  • Original name still: Ada accanto a New object name: Grace dimostra che withName ha prodotto una copia lasciando l'originale intatto.
  • Different instance: true conferma che il wither ha restituito un oggetto genuinamente nuovo anziché lo stesso riferimento.
  • Records equal by value: true e Key still found: origin-ish mostrano che i valori immutabili si confrontano e si hashano per contenuto, rendendoli chiavi HashMap affidabili.

Esercizio

Pratica
Quando una classe immutabile memorizza un campo mutabile come una List, perché bisogna fare una copia difensiva nel costruttore?
Quando una classe immutabile memorizza un campo mutabile come una List, perché bisogna fare una copia difensiva nel costruttore?
Was this page helpful?