W3docs

Melhores Práticas de Imutabilidade em Java

Por que a imutabilidade é um bom padrão em Java e os padrões para construir tipos imutáveis com segurança.

Um objeto imutável é aquele cujo estado não pode mudar após a construção. Essa única propriedade elimina uma classe inteira de bugs: não há mutações surpresa vindas de outra thread, não há surpresas de aliasing onde duas variáveis compartilham estado, e não há necessidade de fazer cópias defensivas em cada leitura. Em Java, a imutabilidade não é automática — você precisa construí-la deliberadamente. Este capítulo cobre os padrões que tornam um tipo verdadeiramente imutável e os hábitos que mantêm essa característica.

Por que a imutabilidade é um bom padrão

Estado mutável compartilhado é a raiz da maioria dos bugs de concorrência e de um número surpreendente de bugs de thread única também. Quando um objeto não pode mudar, você pode passá-lo livremente, armazená-lo em cache e raciocinar sobre ele sem rastrear quem mais possui uma referência.

PropriedadeObjeto mutávelObjeto imutável
Segurança de threadPrecisa de locks ou cuidadoInerentemente thread-safe
Seguro para compartilharNão — chamadores podem mutarSim — distribua a mesma instância
Seguro como chave de mapaArriscado — hashCode pode mudarSim — identidade é estável
CacheDeve invalidar ao mudarCache para sempre
RaciocínioRastreie cada escritaValor é fixo na construção

O custo é alocação: mudar um campo significa criar um novo objeto. Para a maioria do código esse custo é insignificante e a segurança vale a pena. Use mutabilidade somente quando o profiling provar que você precisa.

As cinco regras para uma classe imutável

Uma classe é imutável quando todas as condições a seguir são satisfeitas. Falhar em uma delas permite que um chamador alcance e mude o estado.

  1. A classe é final (ou todos os construtores são privados) para que não possa ser subclassificada com comportamento mutável.
  2. Todos os campos são private final.
  3. Não há setters — nem outros métodos que alterem um campo.
  4. Campos mutáveis são copiados defensivamente na entrada para que a referência do chamador não possa ser usada para mutar seu estado.
  5. Getters nunca expõem diretamente um objeto interno mutável — retorne uma cópia ou uma visão não modificável.
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);
    }
}

Cópias defensivas para campos mutáveis

Primitivos e String já são imutáveis, portanto armazená-los é seguro. O perigo está nos campos mutáveis — arrays, coleções, datas. Se você armazenar a referência do chamador diretamente, ele mantém acesso aos seus 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 e Map.copyOf (Java 10+) realizam ambas as funções de uma vez: copiam os dados e retornam uma visão não modificável. Para arrays, use array.clone() na entrada e clone() novamente na saída, pois arrays são sempre mutáveis e não possuem wrapper somente leitura.

Records: imutabilidade por construção

Um record (Java 16+) é a forma mais concisa de declarar um portador imutável de dados. O compilador gera campos private final, um construtor canônico, acessores e equals/hashCode/toString baseados em 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");
        }
    }
}

Records cobrem o caso comum muito bem, mas não são um escudo mágico: se um componente de record é um tipo mutável (como List), você ainda deve copiá-lo defensivamente no construtor compacto, porque o acessor gerado retorna a referência armazenada como está.

Produzindo cópias modificadas: o padrão "wither"

Como você não pode mutar um objeto imutável, você cria uma cópia modificada. A convenção é um método withX que retorna uma nova instância com um campo alterado e o restante mantido.

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

Isso mantém o original seguro para compartilhamento enquanto permite que os chamadores criem variações. É o mesmo modelo que o JDK usa internamente — LocalDate.plusDays, String.replace e BigDecimal.add todos retornam novas instâncias em vez de mutar o receptor.

Um exemplo completo executável

O programa abaixo constrói um pequeno Account imutável e, em seguida, tenta todos os truques que um chamador poderia usar para mutá-lo — passando uma lista e mutando o original, mutando a lista retornada e renomeando. Ele prova que cada defesa funciona e, em seguida, mostra por que valores imutáveis são chaves de mapa seguras.

java— editable, runs on the server

O que observar na execução:

  • Roles after mutating source: [read, write] prova que a cópia defensiva funcionou — adicionar admin à lista original nunca chegou ao Account.
  • O UnsupportedOperationException em acc.roles().add("hacker") mostra que o getter retornou uma visão não modificável, portanto os chamadores não podem mutar os internos através dele.
  • Original name still: Ada ao lado de New object name: Grace prova que withName produziu uma cópia e deixou o original intacto.
  • Different instance: true confirma que o wither retornou um objeto genuinamente novo em vez da mesma referência.
  • Records equal by value: true e Key still found: origin-ish mostram que valores imutáveis se comparam e geram hash pelo conteúdo, tornando-os chaves de HashMap confiáveis.

Prática

Prática
Quando uma classe imutável armazena um campo mutável como um List, por que você deve fazer uma cópia defensiva no construtor?
Quando uma classe imutável armazena um campo mutável como um List, por que você deve fazer uma cópia defensiva no construtor?
Was this page helpful?