VapVup👤
← Voltar
⚖️

Igualdade e Objetos: O Contrato que Segura o Java

Igualdade em Java parece simples até você perceber que um HashMap pode 'perder' seus objetos, que == nem sempre mente, e que clone() é considerado um dos maiores erros de design da linguagem. Neste módulo, vamos dominar os contratos que todo objeto Java herda — e entender por que quebrá-los causa bugs que nenhum debugger encontra facilmente.

👯Analogia

== vs equals(): Gêmeos e Documentos

Imagine dois gêmeos idênticos. Eles parecem iguais (equals), têm as mesmas características, mas são pessoas diferentes (referências diferentes). O operador == é como perguntar 'é a MESMA pessoa?' — ele compara identidade (endereço de memória). Já equals() é como comparar RGs — verifica se dois objetos são logicamente equivalentes, mesmo sendo instâncias separadas. Quando você faz String s1 = new String("Java") e String s2 = new String("Java"), == retorna false (pessoas diferentes), mas equals() retorna true (mesmo RG).

💡Conceito

Os 5 Mandamentos de equals()

O contrato de equals() define 5 propriedades que DEVEM ser respeitadas: 1. **Reflexivo**: x.equals(x) → true. Um objeto é sempre igual a si mesmo. 2. **Simétrico**: se x.equals(y), então y.equals(x). Sem trapaças — a igualdade é bidirecional. 3. **Transitivo**: se x.equals(y) e y.equals(z), então x.equals(z). Como uma corrente lógica. 4. **Consistente**: chamadas repetidas retornam o mesmo resultado (se os objetos não mudaram). 5. **Não-nulidade**: x.equals(null) → false. Nenhum objeto é igual a null. Violar qualquer um deles pode fazer Collections (HashSet, HashMap, ArrayList.contains) se comportarem de forma imprevisível. O erro mais traiçoeiro é violar a simetria — por exemplo, uma classe CaseInsensitiveString que faz equals com String, mas String não sabe comparar com CaseInsensitiveString.

🔧Exemplo

Implementação Modelo de equals() e hashCode()

Siga esta receita do Effective Java para nunca errar:

public class Produto {
    private final int id;
    private final String nome;
    private final BigDecimal preco;

    @Override
    public boolean equals(Object obj) {
        // 1. Atalho de performance: mesma referência
        if (this == obj) return true;
        // 2. Tipo correto? (cobre null check — null instanceof X é false)
        if (!(obj instanceof Produto)) return false;
        // 3. Cast seguro
        Produto other = (Produto) obj;
        // 4. Comparar campos significativos
        return this.id == other.id
            && Objects.equals(this.nome, other.nome)
            && Objects.equals(this.preco, other.preco);
    }

    @Override
    public int hashCode() {
        // Mesmo campos usados em equals!
        return Objects.hash(id, nome, preco);
    }
}
📮Analogia

hashCode() é o CEP, equals() é o Endereço Completo

Pense no HashMap como um sistema de correios. O hashCode() é o CEP — ele define em qual bairro (bucket) o objeto será armazenado. O equals() é o endereço completo — identifica a casa exata dentro do bairro. Se dois objetos iguais tiverem CEPs diferentes (hashCodes diferentes), a carta vai para o bairro errado e nunca é encontrada. Por isso a regra: se equals() diz que são iguais, hashCode() DEVE retornar o mesmo valor. Mas dois endereços em bairros diferentes podem ter o mesmo CEP (colisão de hash) — isso é normal, o carteiro apenas procura mais.

💡Conceito

Integer Cache e String Pool: Quando == Funciona (Por Acidente)

A JVM otimiza certos objetos, o que pode enganar desenvolvedores: **Integer Cache**: valores de -128 a 127 são cacheados. Integer a = 42 e Integer b = 42 apontam para o MESMO objeto, então == retorna true. Mas Integer a = 200 e Integer b = 200 são objetos diferentes — == retorna false. **String Pool**: literais String são internalizadas. String s1 = "Java" e String s2 = "Java" apontam para o mesmo objeto no pool. Mas new String("Java") cria um objeto NOVO fora do pool. A lição: == funcionar com esses tipos é um acidente da otimização, não um comportamento confiável. SEMPRE use equals() para comparar valores de objetos.

🔧Exemplo

Comparable: Ordenação Natural com compareTo()

Comparable define a ordenação 'natural' de uma classe — aquela que faz sentido intuitivamente.

public class Funcionario implements Comparable<Funcionario> {
    private String nome;
    private double salario;

    @Override
    public int compareTo(Funcionario outro) {
        // Nunca use subtração! Pode dar overflow com int
        // ou truncamento com double.
        int resultado = this.nome.compareTo(outro.nome);
        if (resultado != 0) return resultado;
        // Desempate por salário
        return Double.compare(this.salario, outro.salario);
    }

    // Se compareTo retorna 0, equals() DEVERIA retornar true
    // (consistência com equals — recomendação forte)
}
🔧Exemplo

Comparator: API Fluente do Java 8+

Comparator é muito mais flexível e expressivo desde o Java 8:

// Ordenar por departamento, depois por salário descendente
Comparator<Funcionario> comp = Comparator
    .comparing(Funcionario::getDepartamento)
    .thenComparing(Funcionario::getSalario, Comparator.reverseOrder());

// Tratar nulls com segurança
Comparator<Funcionario> compNullSafe = Comparator
    .comparing(Funcionario::getNome,
        Comparator.nullsLast(Comparator.naturalOrder()));

// Encadear múltiplos critérios
funcionarios.sort(comp);

// Comparator.comparing() extrai a chave de comparação
// e cria o Comparator automaticamente.
// Muito mais legível que classes anônimas!
📋Analogia

clone() é uma Fotocópia Ruim

Imagine que clone() é uma máquina de fotocópia antiga. Ela copia a folha de papel (o objeto), mas se a folha tiver um post-it colado (referência a outro objeto), a fotocópia mostra o MESMO post-it — não cria um novo. Agora se alguém escreve no post-it original, a 'cópia' também muda. Isso é shallow copy. Para uma cópia verdadeira (deep copy), você precisaria copiar cada post-it individualmente e colar na nova folha. E pior: a máquina exige que você coloque um adesivo 'Cloneable' na folha, mas o adesivo não faz nada — é só uma permissão. Se você esquecer o adesivo, a máquina joga um CloneNotSupportedException na sua cara.

💡Conceito

Por que Evitar clone() — Effective Java Item 13

Joshua Bloch lista vários problemas com clone(): 1. **Cloneable não declara clone()**: a interface é um marker interface que magicamente altera o comportamento de Object.clone(). Design terrível. 2. **Construtores não são chamados**: super.clone() aloca memória e copia bits, bypassing toda lógica de construção. 3. **Campos final**: se um campo mutável é final, você não consegue reatribuí-lo no clone() após super.clone(). 4. **Exceções checked**: CloneNotSupportedException é checked, poluindo a API sem benefício real. 5. **Thread safety**: clone() não é synchronized por padrão. **Alternativas recomendadas**: - Copy constructor: new Produto(outroProduto) - Factory method: Produto.copyOf(outroProduto) - Ambos são mais claros, mais seguros e mais flexíveis.

🔧Exemplo

Copy Constructor vs clone()

Compare as duas abordagens — o copy constructor é mais seguro e expressivo:

// ❌ Com clone() — frágil e verboso
public class Pedido implements Cloneable {
    private List<Item> itens;

    @Override
    public Pedido clone() {
        try {
            Pedido copia = (Pedido) super.clone();
            // Precisa copiar manualmente — fácil esquecer!
            copia.itens = new ArrayList<>(this.itens);
            return copia;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // Nunca acontece
        }
    }
}

// ✅ Com copy constructor — claro e seguro
public class Pedido {
    private final List<Item> itens;

    // Copy constructor
    public Pedido(Pedido original) {
        // Campos final funcionam normalmente!
        this.itens = new ArrayList<>(original.itens);
    }

    // Ou factory method
    public static Pedido copyOf(Pedido original) {
        return new Pedido(original);
    }
}