Imutabilidade em Java: Conceitos e Implementação

📋 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

AspectoClasse MutávelClasse Imutável
Thread SafetyRequer sincronizaçãoNaturalmente thread-safe
PerformanceModificação in-placeCriação de novos objetos
ComplexidadeMaior complexidadeMenor complexidade
CacheDifícil de cachearFácil de cachear
DebuggingEstado pode mudarEstado consistente

🎯 Resumo das Melhores Práticas

✅ Checklist para Classe Imutável

  1. Declare a classe como final
  2. Torne todos os campos private final
  3. Não forneça métodos setters
  4. Inicialize campos apenas via construtor
  5. Implemente cópia defensiva para objetos mutáveis
  6. Retorne cópias de objetos mutáveis nos getters
  7. Implemente equals()hashCode() e toString()
  8. Considere usar o padrão Builder para objetos complexos
  9. Valide parâmetros no construtor
  10. 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! 🚀

Rolar para cima