O que é imutabilidade em Java (e por que ela importa)
Um objeto imutável é aquele cujo estado não pode ser alterado após a construção. Em Java, isso significa que, depois que o construtor termina, os valores “observáveis” do objeto permanecem os mesmos durante toda a sua vida.
Imutabilidade traz benefícios práticos:
- Simplicidade: menos estados possíveis, menos combinações para testar e depurar.
- Segurança: invariantes ficam estáveis; quem recebe o objeto não consegue “quebrar” suas regras internas.
- Concorrência: objetos imutáveis são naturalmente thread-safe (não exigem sincronização para leitura).
- Reuso seguro: o mesmo objeto pode ser compartilhado sem medo de efeitos colaterais.
Checklist de um objeto imutável bem construído
1) Campos final e sem setters
O padrão mais comum é: todos os campos são private final, inicializados no construtor, e não existem métodos setters. Se for necessário “alterar” algo, cria-se uma nova instância com os valores desejados.
2) Validação no construtor (invariantes sempre verdadeiras)
Como o estado não muda, o construtor é o lugar central para validar regras. Se o objeto existe, ele é válido.
3) Não expor referências mutáveis (cópias defensivas)
Mesmo que seus campos sejam final, você pode acidentalmente tornar o objeto mutável se armazenar ou retornar referências para objetos mutáveis (como Date, arrays, List, Map).
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Para manter a imutabilidade, use cópias defensivas:
- Ao receber um objeto mutável no construtor: copie antes de armazenar.
- Ao retornar um objeto mutável em um getter: retorne uma cópia (ou uma visão imutável).
Passo a passo: construindo um objeto imutável
Vamos criar um Appointment (agendamento) que guarda um horário e uma lista de participantes. O objetivo é evitar que código externo altere o estado após a criação.
Passo 1: versão imutável usando java.time e cópias defensivas
Prefira tipos imutáveis da API moderna de datas (por exemplo, Instant, LocalDate, LocalDateTime). Eles já são imutáveis e reduzem a necessidade de cópias defensivas.
import java.time.LocalDateTime;import java.util.List;import java.util.Objects;public final class Appointment { private final String title; private final LocalDateTime startsAt; private final List<String> participants; public Appointment(String title, LocalDateTime startsAt, List<String> participants) { this.title = validateTitle(title); this.startsAt = Objects.requireNonNull(startsAt, "startsAt"); Objects.requireNonNull(participants, "participants"); // cópia defensiva + lista não modificável this.participants = List.copyOf(participants); } private static String validateTitle(String title) { Objects.requireNonNull(title, "title"); String trimmed = title.trim(); if (trimmed.isEmpty()) throw new IllegalArgumentException("title vazio"); return trimmed; } public String getTitle() { return title; } public LocalDateTime getStartsAt() { return startsAt; // LocalDateTime é imutável } public List<String> getParticipants() { return participants; // já é não-modificável (List.copyOf) } // "alterações" retornam um novo objeto public Appointment withTitle(String newTitle) { return new Appointment(newTitle, this.startsAt, this.participants); } public Appointment addParticipant(String name) { Objects.requireNonNull(name, "name"); var newList = new java.util.ArrayList<String>(this.participants); newList.add(name); return new Appointment(this.title, this.startsAt, newList); }}Pontos importantes do exemplo:
final class: impede subclasses de introduzirem mutabilidade (por exemplo, sobrescrevendo getters para expor estado mutável). Nem sempre é obrigatório, mas é uma proteção comum.List.copyOf: cria uma cópia e retorna uma lista não-modificável; se a lista original mudar, o objeto não é afetado.- Métodos
withX/addX: modelam mudanças como criação de nova instância.
Passo 2: quando você recebe Date (mutável) — cópia defensiva obrigatória
Se você precisar interoperar com APIs legadas que usam java.util.Date, trate Date como mutável e faça cópias defensivas no construtor e no getter.
import java.util.Date;import java.util.Objects;public final class LegacyDeadline { private final Date dueAt; public LegacyDeadline(Date dueAt) { Objects.requireNonNull(dueAt, "dueAt"); // cópia defensiva na entrada this.dueAt = new Date(dueAt.getTime()); } public Date getDueAt() { // cópia defensiva na saída return new Date(dueAt.getTime()); }}Sem essas cópias, alguém poderia fazer getDueAt().setTime(...) e alterar o estado interno do objeto.
Passo 3: arrays e coleções mutáveis
Arrays são mutáveis por definição. Se você precisa armazenar um array, copie-o ao receber e ao retornar.
import java.util.Arrays;import java.util.Objects;public final class Hash256 { private final byte[] bytes; public Hash256(byte[] bytes) { Objects.requireNonNull(bytes, "bytes"); if (bytes.length != 32) throw new IllegalArgumentException("hash deve ter 32 bytes"); this.bytes = Arrays.copyOf(bytes, bytes.length); } public byte[] toByteArray() { return Arrays.copyOf(bytes, bytes.length); }}Armadilhas comuns ao tentar ser imutável
Expor referência interna por getter
Mesmo com campo final, isto quebra a imutabilidade:
public final class Bad { private final java.util.List<String> items; public Bad(java.util.List<String> items) { this.items = items; // guarda referência externa (ruim) } public java.util.List<String> getItems() { return items; // devolve referência mutável (ruim) }}Correção: this.items = List.copyOf(items) e retornar a lista não-modificável.
Validação incompleta no construtor
Se o objeto é imutável, qualquer estado inválido que passar pelo construtor ficará “congelado”. Portanto, valide tudo que for necessário para manter invariantes.
Subclasses adicionando mutabilidade
Se a classe não for final, uma subclasse pode introduzir setters, expor referências internas ou alterar o comportamento de getters. Se você pretende garantir imutabilidade como contrato, tornar a classe final é uma defesa simples.
Value Objects: o que são e quando usar
Value Object é um objeto definido pelos seus valores, não por identidade. Em geral:
- Dois value objects com os mesmos valores são considerados equivalentes.
- São excelentes candidatos a imutabilidade.
- Representam conceitos do domínio:
Money,Email,CPF,Coordinates,Percentage.
Use value objects quando você quer:
- Centralizar validação (ex.: formato de e-mail) em um tipo dedicado.
- Evitar tipos primitivos ambíguos (ex.:
Stringpara e-mail,BigDecimalpara dinheiro sem moeda). - Reduzir estados inválidos no restante do sistema.
Exemplo: Email como value object imutável
import java.util.Objects;import java.util.regex.Pattern;public final class Email { private static final Pattern BASIC = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"); private final String value; public Email(String value) { Objects.requireNonNull(value, "value"); String normalized = value.trim().toLowerCase(); if (!BASIC.matcher(normalized).matches()) { throw new IllegalArgumentException("email inválido"); } this.value = normalized; } public String value() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Email other)) return false; return value.equals(other.value); } @Override public int hashCode() { return value.hashCode(); } @Override public String toString() { return value; }}Observações práticas:
equals/hashCodesão baseados no valor, o que facilita uso em coleções e comparações.- O restante do código passa a receber
Emailem vez deString, reduzindo validações repetidas.
Exemplo: Money (valor + moeda) como value object
import java.math.BigDecimal;import java.util.Currency;import java.util.Objects;public final class Money { private final BigDecimal amount; private final Currency currency; public Money(BigDecimal amount, Currency currency) { Objects.requireNonNull(amount, "amount"); this.currency = Objects.requireNonNull(currency, "currency"); // normalização: escala conforme moeda (ex.: 2 casas para BRL) int scale = currency.getDefaultFractionDigits(); this.amount = amount.setScale(scale, java.math.RoundingMode.HALF_UP); } public BigDecimal amount() { return amount; } public Currency currency() { return currency; } public Money plus(Money other) { requireSameCurrency(other); return new Money(this.amount.add(other.amount), currency); } private void requireSameCurrency(Money other) { Objects.requireNonNull(other, "other"); if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("moedas diferentes"); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money other)) return false; return amount.equals(other.amount) && currency.equals(other.currency); } @Override public int hashCode() { return Objects.hash(amount, currency); }}Atividade prática: converter uma entidade mutável em imutável e ajustar o código
Cenário inicial (mutável)
Você recebeu uma classe Customer que é usada em vários pontos do sistema. Ela é mutável e expõe uma lista interna.
import java.util.ArrayList;import java.util.Date;import java.util.List;public class Customer { private String name; private Date birthDate; private List<String> tags = new ArrayList<>(); public Customer() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; }}Objetivo
- Tornar
Customerimutável. - Proteger contra mutabilidade de
DateeList. - Atualizar o código chamador para trabalhar com a nova API (sem setters).
Passo a passo
1) Transforme a classe em final e os campos em private final
Remova o construtor vazio e exija os dados no construtor.
2) Valide e normalize no construtor
Garanta que name não seja vazio e que as referências não sejam nulas.
3) Aplique cópias defensivas para Date e List
Copie Date na entrada e na saída. Para tags, use List.copyOf.
4) Substitua setters por métodos que retornam nova instância
Implemente withName, withBirthDate, withAddedTag (ou similares).
Implementação sugerida (imutável)
import java.util.Date;import java.util.List;import java.util.Objects;public final class Customer { private final String name; private final Date birthDate; private final List<String> tags; public Customer(String name, Date birthDate, List<String> tags) { this.name = validateName(name); this.birthDate = copyDate(birthDate); Objects.requireNonNull(tags, "tags"); this.tags = List.copyOf(tags); } private static String validateName(String name) { Objects.requireNonNull(name, "name"); String trimmed = name.trim(); if (trimmed.isEmpty()) throw new IllegalArgumentException("name vazio"); return trimmed; } private static Date copyDate(Date d) { Objects.requireNonNull(d, "birthDate"); return new Date(d.getTime()); } public String getName() { return name; } public Date getBirthDate() { return new Date(birthDate.getTime()); } public List<String> getTags() { return tags; } public Customer withName(String newName) { return new Customer(newName, this.birthDate, this.tags); } public Customer withBirthDate(Date newBirthDate) { return new Customer(this.name, newBirthDate, this.tags); } public Customer withAddedTag(String tag) { Objects.requireNonNull(tag, "tag"); var newTags = new java.util.ArrayList<String>(this.tags); newTags.add(tag); return new Customer(this.name, this.birthDate, newTags); }}Ajuste do restante do código (exemplos)
Antes (mutável):
Customer c = new Customer();c.setName("Ana");c.setBirthDate(new Date());c.getTags().add("premium");Depois (imutável):
Customer c = new Customer("Ana", new Date(), java.util.List.of());c = c.withAddedTag("premium");Se você tinha um método que “atualizava” o cliente:
// antespublic void upgradeToPremium(Customer c) { c.getTags().add("premium");}Reescreva para retornar a nova instância:
// depoispublic Customer upgradeToPremium(Customer c) { return c.withAddedTag("premium");}Checklist de verificação da atividade
| Item | Como verificar |
|---|---|
| Sem setters | Não existe nenhum método setX público |
Campos final | Todos os atributos são private final |
| Validação no construtor | Entradas inválidas geram exceção e não criam instância |
| Cópias defensivas | Date é copiado na entrada e na saída; coleções usam List.copyOf |
| Alterações por nova instância | Métodos withX retornam um novo Customer |