Imutabilidade em Java OOP: objetos seguros, final, cópias defensivas e value objects

Capítulo 10

Tempo estimado de leitura: 10 minutos

+ Exercício

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).

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.: String para e-mail, BigDecimal para 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/hashCode são baseados no valor, o que facilita uso em coleções e comparações.
  • O restante do código passa a receber Email em vez de String, 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 Customer imutável.
  • Proteger contra mutabilidade de Date e List.
  • 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

ItemComo verificar
Sem settersNão existe nenhum método setX público
Campos finalTodos os atributos são private final
Validação no construtorEntradas inválidas geram exceção e não criam instância
Cópias defensivasDate é copiado na entrada e na saída; coleções usam List.copyOf
Alterações por nova instânciaMétodos withX retornam um novo Customer

Agora responda o exercício sobre o conteúdo:

Ao tornar uma classe como Customer imutável em Java, qual prática ajuda a evitar que código externo altere o estado interno quando há campos mutáveis como Date e List?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Mesmo com campos final, referências para objetos mutáveis podem permitir alterações indiretas. Cópias defensivas em Date (entrada e saída) e List.copyOf para coleções evitam que mudanças externas afetem o estado interno.

Próximo capitúlo

equals e hashCode em Java OOP: identidade, igualdade e uso correto em coleções

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
59%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.