CodeGym /Cursos /JAVA 25 SELF /Contratos equals e hashCode

Contratos equals e hashCode

JAVA 25 SELF
Nível 29 , Lição 0
Disponível

1. Introdução

Como comparamos objetos

Em Java, objetos — não são apenas dados, cada um tem seu próprio endereço na memória. O operador == responde à pergunta “é a mesma caixinha?”, ou seja, compara referências (endereços), não o conteúdo. O método equals destina-se a comparar justamente o conteúdo. Por padrão, se não for sobrescrito, equals se comporta como ==.

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — são objetos diferentes na memória!

Para que dois objetos diferentes sejam considerados iguais pelos dados (por exemplo, todos os campos significativos coincidem), é necessário sobrescrever equals. E, se você planeja usar os objetos em coleções baseadas em hash, é obrigatório sobrescrever corretamente também o hashCode.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // é o mesmo objeto
        if (o == null || getClass() != o.getClass()) return false; // verificamos a classe
        Person person = (Person) o; // fazemos o cast para o tipo necessário
        return age == person.age && name.equals(person.name); // comparamos os campos
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // para funcionar com HashSet/HashMap
    }
}

Tal sobrescrita é crítica para o funcionamento correto com HashSet/HashMap: sem equals e hashCode, as coleções considerarão diferentes até mesmo objetos iguais pelos dados.

A relação de equivalência em equals depende dos seus requisitos: é possível comparar por todos os campos, por parte deles (por exemplo, apenas o email em User) — o importante é a consistência e o cumprimento do contrato.

Onde isso é especialmente importante?

  • Em coleções baseadas em tabelas de hash: HashSet, HashMap, LinkedHashSet e outros.
  • Ao buscar e remover elementos nas coleções: sem um equals correto, o objeto desejado pode “não ser encontrado”.
  • Na lógica de negócio: por exemplo, dois User com o mesmo e-mail devem ser considerados o mesmo usuário.

hashCode — para que ele serve?

Coleções baseadas em hash (por exemplo, HashSet, HashMap) usam tabelas de hash. O método hashCode calcula um número inteiro — o “endereço do bucket” para onde o objeto irá. Se dois objetos são iguais de acordo com equals, seus hashCode devem coincidir. Se você quebrar essa regra, as coleções começarão a se comportar de forma imprevisível.

2. Contrato de equals e hashCode

Contrato de equals

Requisitos principais para o comportamento de equals:

  • Reflexividade: a.equals(a) é sempre true.
  • Simetria: se a.equals(b) é true, então b.equals(a) também é true.
  • Transitividade: se a.equals(b) e b.equals(c), então a.equals(c) também é true.
  • Consistência: se os objetos não mudam, o resultado das chamadas permanece estável.
  • Comparação com null: qualquer objeto não é igual a null.

Contrato de hashCode

  • Se dois objetos são iguais por equals, seus hashCode são iguais.
  • Se os objetos não são iguais, seus hash codes podem coincidir (colisões são permitidas, mas indesejáveis).
  • Enquanto o objeto não muda logicamente, seu hashCode deve permanecer constante.

Em outras palavras, hashCode coincidente é condição necessária, mas não suficiente de igualdade: o mesmo hash não garante igualdade por equals.

3. Implementação de equals e hashCode: exemplo

Considere a classe Person, em que a igualdade é definida pelos campos name e age.

public class Person {
    private String name;
    private int age;

    // Construtor, getters, setters...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Comparação de referências
        if (o == null || getClass() != o.getClass()) return false; // Verificação da classe

        Person person = (Person) o; // Conversão de tipo

        // Comparamos os campos
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 — uma escolha comum de número primo
        return result;
    }
}
  • Primeiro, verificações rápidas: referência e classe.
  • Depois, a comparação dos campos significativos.
  • Em hashCode, usa-se o número primo 31 para reduzir colisões.

Uso de Objects.equals e Objects.hash

A partir do Java 7, a classe Objects simplifica o código e o torna mais seguro em relação a null:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals, hashCode e compareTo: como eles se relacionam

Como equals e compareTo estão relacionados?

A interface Comparable define o método compareTo, que retorna um número negativo/zero/positivo para “menor/igual/maior”. É desejável que de a.compareTo(b) == 0 se siga a.equals(b). O inverso não é obrigatório.

Se essa consistência for violada (por exemplo, compareTo compara apenas por idade, enquanto equals compara por nome e idade), coleções ordenadas como TreeSet/TreeMap podem se comportar de forma inesperada: os objetos são considerados “iguais” do ponto de vista da ordenação, mas não iguais pelo conteúdo.

equals e hashCode em coleções

  • Em HashSet e HashMap, as operações de adicionar/buscar/remover dependem da implementação correta de equals e hashCode.
  • Sem sobrescrever, essas coleções consideram os objetos “diferentes”, mesmo que seus dados sejam iguais.

5. Exemplos: como isso funciona nas coleções

HashSet: armazenamento de objetos únicos

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplicado

System.out.println(people.size()); // 1, se equals/hashCode estiverem implementados corretamente

Sem um equals/hashCode corretos, ambos os objetos entrarão no conjunto.

HashMap: busca por chave

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "Usuário 1");
System.out.println(map.get(p2)); // "Usuário 1", se equals/hashCode estiverem implementados corretamente

Sem o contrato, a coleção retornará null — para ela, são “chaves diferentes”.

6. Boas práticas: dicas de implementação

  • Inclua em equals/hashCode todos os campos que definem a “identidade” do objeto.
  • Não use campos mutáveis (que mudam após a inserção nas coleções) no cálculo do hashCode.
  • Deixe a IDE gerar os métodos — menos chances de erros de digitação.
  • Em equals, verifique primeiro this == o, depois a classe, e então os campos.
  • Para comparar campos-objeto, use Objects.equals.
  • Para o hash code, use Objects.hash ou um padrão consagrado com multiplicador 31.

7. Nuances úteis

Por que não dá para usar apenas hashCode?

Colisões são inevitáveis: objetos diferentes podem ter o mesmo hashCode. O hash é apenas um direcionamento rápido para o bucket; a decisão final sobre igualdade é tomada por equals.

É possível não sobrescrever equals e hashCode?

Somente se você tiver certeza de que os objetos nunca serão comparados por conteúdo e não serão chaves/elementos únicos em coleções. Na prática, isso é raro.

Diferença entre ==, equals e compareTo

Operador/método O que compara? Para que serve?
==
Referências (endereços na memória) Verificar “é o mesmo objeto?”
equals
Conteúdo dos objetos Igualdade segundo a lógica de negócio
compareTo
Ordem (menor/igual/maior) Ordenação

8. Erros típicos ao implementar equals e hashCode

Erro nº 1: sobrescreveu equals, mas esqueceu hashCode. Os objetos são considerados iguais, mas caem em buckets diferentes da tabela de hash — busca e remoção quebram.

Erro nº 2: usar campos mutáveis em hashCode. Se um campo mudar após a inserção na coleção, o objeto “se perde”: o hash muda, mas o bucket não.

Erro nº 3: simetria/transitividade de equals violada. a.equals(b) dá true, mas b.equals(a) — false, ou a transitividade quebra — as coleções começam a se comportar de forma imprevisível.

Erro nº 4: não verificar a classe em equals. Comparar objetos de classes diferentes leva a resultados incorretos ou exceções.

Erro nº 5: comparar strings e objetos com ==. O operador == compara referências; use equals para o conteúdo.

Erro nº 6: falta de consistência entre compareTo e equals. Se a.compareTo(b) == 0, mas !a.equals(b), as coleções TreeSet/TreeMap podem considerar elementos iguais para a ordenação, mas diferentes quanto à igualdade — isso é fonte de “fantasmas” e duplicatas.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION