HashMap em Java: Entendendo a Necessidade de Sobrescrever hashCode() e equals()

Ao utilizar HashMap no Java com objetos personalizados como chave, é fundamental sobrescrever os métodos hashCode() e equals(). O mesmo vale para métodos como contains() em coleções como ArrayList. Entenda onde e por que esses métodos são exigidos.

O HashMap implementa uma tabela hash baseada no código hash dos objetos. Cada chave gera um valor inteiro (hashCode) que determina o bucket onde o par chave-valor será armazenado. Durante a inserção ou busca, a coleção primeiro compara os códigos hash; se houver colisão, o método equals é utilizado para verificar se as chaves são logicamente equivalentes. Por isso, ao usar classes próprias como chave, é indispensável que ambas as implemantações estejam alinhadas.

A implementação padrão de hashCode() herdada de Object retorna o endereço de memória do objeto. Consequentemente, dois objetos com o mesmo conteúdo lógico (mesmo estado) terão códigos hash diferentes, fazendo com que HashMap os trate como chaves distintas. O mesmo ocorre com equals() padrão, que também compara referências. Para que objetos "iguais" semanticamente sejam tratados como a mesma chave, precisamos redefinir ambos os métodos.

Exemplo com HashMap

Considere as classes Usuario e Arquivo abaixo. Sem a sobrescrita adequada, a busca por uma chave logicamente igual retornará null.

import java.util.HashMap;
import java.util.Map;

public class ExemploHashMap {
    public static void main(String[] args) {
        Map<Usuario, Arquivo> mapa = new HashMap<>();

        mapa.put(new Usuario("joao", "joao@email.com"), new Arquivo("relatorio.pdf", "conteudo A"));
        mapa.put(new Usuario("admin", "admin@empresa.com"), new Arquivo("config.xml", "dados B"));

        // Sem hashCode e equals sobrescritos, o retorno é null
        Arquivo resultado = mapa.get(new Usuario("admin", "admin@empresa.com"));
        System.out.println("Resultado da busca: " + resultado);
    }
}

class Usuario {
    private String nome;
    private String email;

    public Usuario(String nome, String email) {
        this.nome = nome;
        this.email = email;
    }

    // Getters e setters omitidos para brevidade

    @Override
    public int hashCode() {
        final int PRIMO = 37;
        int hash = 1;
        hash = PRIMO * hash + (email == null ? 0 : email.hashCode());
        hash = PRIMO * hash + (nome == null ? 0 : nome.hashCode());
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Usuario outro = (Usuario) obj;
        return java.util.Objects.equals(nome, outro.nome) &&
               java.util.Objects.equals(email, outro.email);
    }
}

class Arquivo {
    private String titulo;
    private String conteudo;

    public Arquivo(String titulo, String conteudo) {
        this.titulo = titulo;
        this.conteudo = conteudo;
    }

    @Override
    public String toString() {
        return "Arquivo{" + "titulo='" + titulo + '\'' + ", conteudo='" + conteudo + '\'' + '}';
    }

    @Override
    public int hashCode() {
        final int PRIMO = 37;
        int hash = 1;
        hash = PRIMO * hash + (conteudo == null ? 0 : conteudo.hashCode());
        hash = PRIMO * hash + (titulo == null ? 0 : titulo.hashCode());
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Arquivo outro = (Arquivo) obj;
        return java.util.Objects.equals(titulo, outro.titulo) &&
               java.util.Objects.equals(conteudo, outro.conteudo);
    }
}

Com a sobrescrita, o método get encontra a chave corretamente, pois os códigos hash coincidem e equals retorna verdadeiro para objetos semanticamente iguais.

Exemplo com ArrayList e contains

O mesmo princípio se aplica a ArrayList.contains(). O método intenramente percorre a lista e chama equals para cada elemento. Sem a sobrescrita, mesmo objetos com conteúdo idêntico são considerados diferentes.

import java.util.ArrayList;
import java.util.List;

public class ExemploArrayList {
    public static void main(String[] args) {
        List<Arquivo> lista = new ArrayList<>();
        lista.add(new Arquivo("contrato.pdf", "cláusulas"));
        lista.add(new Arquivo("nota_fiscal.xml", "valores"));

        // Sem equals sobrescrito, retorna false
        boolean existe = lista.contains(new Arquivo("contrato.pdf", "cláusulas"));
        System.out.println("Elemento encontrado? " + existe);
    }
}

Após sobrescrever equals (e, por consistência, hashCode), o método contains retorna true para objetos logicamente equivalentes.

Em resumo, sempre que uma classe for usada como chave em estruturas baseadas em hash (HashMap, HashSet) ou quando a igualdade lógica for necessária em coleções (ArrayList.contains, remove, etc.), é obrigatório sobrescrever hashCode() e equals() de forma consistente.

Tags: HashMap java hashCode equals ArrayList

Publicado em 7-10 08:10