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.
| Propriedade | Objeto mutável | Objeto imutável |
|---|---|---|
| Segurança de thread | Precisa de locks ou cuidado | Inerentemente thread-safe |
| Seguro para compartilhar | Não — chamadores podem mutar | Sim — distribua a mesma instância |
| Seguro como chave de mapa | Arriscado — hashCode pode mudar | Sim — identidade é estável |
| Cache | Deve invalidar ao mudar | Cache para sempre |
| Raciocínio | Rastreie cada escrita | Valor é 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.
- A classe é
final(ou todos os construtores são privados) para que não possa ser subclassificada com comportamento mutável. - Todos os campos são
private final. - Não há setters — nem outros métodos que alterem um campo.
- 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.
- 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.
O que observar na execução:
Roles after mutating source: [read, write]prova que a cópia defensiva funcionou — adicionaradminà lista original nunca chegou aoAccount.- O
UnsupportedOperationExceptionemacc.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: Adaao lado deNew object name: Graceprova quewithNameproduziu uma cópia e deixou o original intacto.Different instance: trueconfirma que o wither retornou um objeto genuinamente novo em vez da mesma referência.Records equal by value: trueeKey still found: origin-ishmostram que valores imutáveis se comparam e geram hash pelo conteúdo, tornando-os chaves deHashMapconfiáveis.