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.
== 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).
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.
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);
}
}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.
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.
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)
}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!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.
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.
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);
}
}