
📋 O que é Imutabilidade?
Imutabilidade é um conceito fundamental na programação onde um objeto, uma vez criado, não pode ter seu estado alterado. Em Java, um objeto imutável é aquele cujos valores de seus campos não podem ser modificados após a construção do objeto.
Vantagens da Imutabilidade
- Thread Safety: Objetos imutáveis são naturalmente thread-safe
- Facilidade de Cache: Podem ser facilmente armazenados em cache
- Prevenção de Efeitos Colaterais: Eliminam modificações acidentais
- Simplicidade: Reduzem a complexidade do código
- Hashcode Consistente: O hashcode permanece constante
🔧 Como Criar uma Classe Verdadeiramente Imutável
Para criar uma classe imutável em Java, você deve seguir estas regras fundamentais:
1. Declare a Classe como final
public final class Pessoa {
// implementação
}
2. Torne Todos os Campos private
e final
public final class Pessoa {
private final String nome;
private final int idade;
private final List<String> hobbies;
// construtor e métodos
}
3. Não Forneça Métodos Setters
// ❌ EVITE - Métodos setters quebram a imutabilidade
public void setNome(String nome) {
this.nome = nome; // Isso tornaria a classe mutável
}
4. Inicialize Todos os Campos via Construtor
public final class Pessoa {
private final String nome;
private final int idade;
private final List<String> hobbies;
public Pessoa(String nome, int idade, List<String> hobbies) {
this.nome = nome;
this.idade = idade;
// Cópia defensiva para objetos mutáveis
this.hobbies = new ArrayList<>(hobbies);
}
}
5. Implemente Cópia Defensiva para Objetos Mutáveis
public final class Pessoa {
private final String nome;
private final int idade;
private final List<String> hobbies;
public Pessoa(String nome, int idade, List<String> hobbies) {
this.nome = nome;
this.idade = idade;
// ✅ Cópia defensiva no construtor
this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
}
// ✅ Cópia defensiva no getter
public List<String> getHobbies() {
return new ArrayList<>(hobbies);
}
}
💡 Exemplo Completo de Classe Imutável
import java.util.*;
public final class ContaBancaria {
private final String titular;
private final String numeroConta;
private final double saldo;
private final List<String> transacoes;
private final Date dataAbertura;
public ContaBancaria(String titular, String numeroConta, double saldo,
List<String> transacoes, Date dataAbertura) {
this.titular = titular;
this.numeroConta = numeroConta;
this.saldo = saldo;
// Cópia defensiva para objetos mutáveis
this.transacoes = Collections.unmodifiableList(
new ArrayList<>(transacoes)
);
this.dataAbertura = new Date(dataAbertura.getTime());
}
// Apenas getters, sem setters
public String getTitular() {
return titular;
}
public String getNumeroConta() {
return numeroConta;
}
public double getSaldo() {
return saldo;
}
public List<String> getTransacoes() {
// Retorna uma nova lista para evitar modificações
return new ArrayList<>(transacoes);
}
public Date getDataAbertura() {
// Retorna uma nova data para evitar modificações
return new Date(dataAbertura.getTime());
}
// Métodos que retornam novas instâncias em vez de modificar
public ContaBancaria depositar(double valor, String descricao) {
List<String> novasTransacoes = new ArrayList<>(this.transacoes);
novasTransacoes.add("Depósito: " + valor + " - " + descricao);
return new ContaBancaria(
this.titular,
this.numeroConta,
this.saldo + valor,
novasTransacoes,
this.dataAbertura
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
ContaBancaria that = (ContaBancaria) obj;
return Double.compare(that.saldo, saldo) == 0 &&
Objects.equals(titular, that.titular) &&
Objects.equals(numeroConta, that.numeroConta) &&
Objects.equals(transacoes, that.transacoes) &&
Objects.equals(dataAbertura, that.dataAbertura);
}
@Override
public int hashCode() {
return Objects.hash(titular, numeroConta, saldo, transacoes, dataAbertura);
}
@Override
public String toString() {
return "ContaBancaria{" +
"titular='" + titular + '\'' +
", numeroConta='" + numeroConta + '\'' +
", saldo=" + saldo +
", transacoes=" + transacoes.size() + " transações" +
", dataAbertura=" + dataAbertura +
'}';
}
}
🎯 Exemplo de Uso
public class ExemploUso {
public static void main(String[] args) {
List<String> transacoesIniciais = Arrays.asList(
"Depósito inicial: 1000.00"
);
ContaBancaria conta = new ContaBancaria(
"Bianeck",
"12345-6",
1000.0,
transacoesIniciais,
new Date()
);
// ✅ Operações que retornam novas instâncias
ContaBancaria contaAtualizada = conta.depositar(500.0, "Salário");
System.out.println("Conta original: " + conta.getSaldo()); // 1000.0
System.out.println("Conta atualizada: " + contaAtualizada.getSaldo()); // 1500.0
// ✅ Tentativa de modificar a lista retornada não afeta o objeto original
List<String> transacoes = conta.getTransacoes();
transacoes.add("Tentativa de modificação"); // Não afeta o objeto original
System.out.println("Transações originais: " + conta.getTransacoes().size()); // 1
}
}
🔍 Padrões Avançados para Imutabilidade
Builder Pattern para Objetos Complexos
public final class Endereco {
private final String rua;
private final String cidade;
private final String estado;
private final String cep;
private Endereco(Builder builder) {
this.rua = builder.rua;
this.cidade = builder.cidade;
this.estado = builder.estado;
this.cep = builder.cep;
}
public static class Builder {
private String rua;
private String cidade;
private String estado;
private String cep;
public Builder rua(String rua) {
this.rua = rua;
return this;
}
public Builder cidade(String cidade) {
this.cidade = cidade;
return this;
}
public Builder estado(String estado) {
this.estado = estado;
return this;
}
public Builder cep(String cep) {
this.cep = cep;
return this;
}
public Endereco build() {
return new Endereco(this);
}
}
// Getters
public String getRua() { return rua; }
public String getCidade() { return cidade; }
public String getEstado() { return estado; }
public String getCep() { return cep; }
}
Uso com Records (Java 14+)
public record PessoaRecord(String nome, int idade, List<String> hobbies) {
public PessoaRecord {
// Validação e cópia defensiva no construtor compacto
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Nome não pode ser vazio");
}
if (idade < 0) {
throw new IllegalArgumentException("Idade não pode ser negativa");
}
// Cópia defensiva
hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
}
public List<String> hobbies() {
return new ArrayList<>(hobbies);
}
}
⚠️ Armadilhas Comuns
1. Objetos Mutáveis como Campos
// ❌ PROBLEMÁTICO
public final class ProblematicClass {
private final Date data;
public ProblematicClass(Date data) {
this.data = data; // Referência direta - perigoso!
}
public Date getData() {
return data; // Retorna referência mutável
}
}
// ✅ CORRETO
public final class CorrectClass {
private final Date data;
public CorrectClass(Date data) {
this.data = new Date(data.getTime()); // Cópia defensiva
}
public Date getData() {
return new Date(data.getTime()); // Retorna cópia
}
}
2. Herança Não Controlada
// ❌ PROBLEMÁTICO - Classe não final
public class BaseClass {
private final String valor;
public BaseClass(String valor) {
this.valor = valor;
}
public String getValor() {
return valor;
}
}
// Subclasse pode quebrar a imutabilidade
public class SubClass extends BaseClass {
private String valorMutavel;
public SubClass(String valor) {
super(valor);
}
public void setValorMutavel(String valor) {
this.valorMutavel = valor; // Quebra a imutabilidade
}
}
📊 Comparação: Mutável vs Imutável
Aspecto | Classe Mutável | Classe Imutável |
---|---|---|
Thread Safety | Requer sincronização | Naturalmente thread-safe |
Performance | Modificação in-place | Criação de novos objetos |
Complexidade | Maior complexidade | Menor complexidade |
Cache | Difícil de cachear | Fácil de cachear |
Debugging | Estado pode mudar | Estado consistente |
🎯 Resumo das Melhores Práticas
✅ Checklist para Classe Imutável
- Declare a classe como
final
- Torne todos os campos
private final
- Não forneça métodos setters
- Inicialize campos apenas via construtor
- Implemente cópia defensiva para objetos mutáveis
- Retorne cópias de objetos mutáveis nos getters
- Implemente
equals()
,hashCode()
etoString()
- Considere usar o padrão Builder para objetos complexos
- Valide parâmetros no construtor
- Use
Collections.unmodifiableList()
quando apropriado
🔧 Ferramentas Úteis
- Lombok:
@Value
para classes imutáveis - Records: Para classes de dados simples (Java 14+)
- Google Guava: Coleções imutáveis
- Apache Commons: Utilitários para cópia defensiva
A imutabilidade é uma ferramenta poderosa para criar código mais seguro, previsível e fácil de manter. Embora possa haver um pequeno overhead de performance devido à criação de novos objetos, os benefícios em termos de segurança e simplicidade geralmente superam esse custo! 🚀