Guia para hashCode () em Java

Guia para hashCode () em Java

*1. Visão geral *

Hashing é um conceito fundamental da ciência da computação.

Em Java, algoritmos de hash eficientes estão por trás de algumas das coleções mais populares que temos disponíveis, como as https://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html [HashMap] _ (para uma visão detalhada de _HashMap, sinta-se à vontade para verificar o link:/java-hashmap [este artigo]) e os https://docs.oracle.com/javase/7/docs/api/java/util/HashSet .html [HashSet] .

Neste artigo, vamos nos concentrar em como _hashCode () _ funciona, como ele é reproduzido nas coleções e como implementá-lo corretamente.

===* 2. Uso de _hashCode () _ em estruturas de dados *

As operações mais simples em coleções podem ser ineficientes em determinadas situações.

Por exemplo, isso aciona uma pesquisa linear que é altamente ineficaz para listas de tamanhos grandes:

List<String> words = Arrays.asList("Welcome", "to", "Baeldung");
if (words.contains("Baeldung")) {
    System.out.println("Baeldung is in the list");
}

O Java fornece várias estruturas de dados para lidar com esse problema especificamente - por exemplo, várias implementações da interface Map são hash tabelas.

Ao usar uma tabela de hash,* essas coleções calculam o valor de hash para uma determinada chave usando o método _hashCode () _ *e usam esse valor internamente para armazenar os dados - para que as operações de acesso sejam muito mais eficientes.

===* 3. Compreendendo como _hashCode () _ funciona *

Simplificando, _hashCode () _ retorna um valor inteiro, gerado por um algoritmo de hash.

Objetos iguais (de acordo com seus _equals () _) devem retornar o mesmo código de hash.* Não é necessário que objetos diferentes retornem códigos de hash diferentes. *

O contrato geral de _hashCode () _ declara:

  • Sempre que é chamado no mesmo objeto mais de uma vez durante a execução de um aplicativo Java, _hashCode () _ deve retornar consistentemente o mesmo valor, desde que nenhuma informação usada em comparações iguais no objeto seja modificada. Esse valor não precisa permanecer consistente de uma execução de um aplicativo para outra execução do mesmo aplicativo

  • Se dois objetos forem iguais de acordo com o método _equals (Object) _, a chamada do método _hashCode () _ em cada um dos dois objetos deverá produzir o mesmo valor

    *Não é necessário que, se dois objetos forem desiguais, de acordo com o https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals%28java.lang.Object%29[_equals (java.lang.Object) _] e, em seguida, chamar o método _hashCode_ em cada um dos dois objetos deve produzir resultados inteiros distintos. No entanto, os desenvolvedores devem estar cientes de que a produção de resultados inteiros distintos para objetos desiguais melhora o desempenho das tabelas de hash

_ “Tanto quanto for razoavelmente prático, o método _hashCode () _ definido pela classe _Object retorna números inteiros distintos para objetos distintos. (Isso geralmente é implementado convertendo o endereço interno do objeto em um número inteiro, mas essa técnica de implementação não é requerida pela linguagem de programação JavaTM.) ” __

===* 4. Uma implementação _hashCode () _ Naive *

Na verdade, é bastante simples ter uma implementação ingênua _hashCode () _ que adere totalmente ao contrato acima.

Para demonstrar isso, vamos definir uma amostra de classe User que substitui a implementação padrão do método:

public class User {

    private long id;
    private String name;
    private String email;

   //standard getters/setters/constructors

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
          && (name.equals(user.name)
          && email.equals(user.email));
    }

   //getters and setters here
}

A classe User fornece implementações personalizadas para _equals () _ e _hashCode () _ que aderem totalmente aos respectivos contratos. Ainda mais, não há nada ilegítimo em ter _hashCode () _ retornando qualquer valor fixo.

*No entanto, essa implementação degrada a funcionalidade das tabelas de hash para basicamente zero, pois cada objeto seria armazenado no mesmo bloco único.

Nesse contexto, uma pesquisa de tabela de hash é realizada linearmente e não nos oferece nenhuma vantagem real - mais sobre isso na seção 7.

===* 5. Melhorando a implementação _hashCode () _ *

Vamos melhorar um pouco a implementação atual hashCode () _ incluindo todos os campos da classe _User para que ela possa produzir resultados diferentes para objetos desiguais:

@Override
public int hashCode() {
    return (int) id* name.hashCode() *email.hashCode();
}

Esse algoritmo de hash básico é definitivamente muito melhor que o anterior, pois calcula o código de hash do objeto apenas multiplicando os códigos de hash dos campos name e email e o id.

Em termos gerais, podemos dizer que esta é uma implementação razoável de _hashCode () _, desde que mantenhamos a implementação _equals () _ consistente com ela.

===* 6. _HashCode () _ implementações padrão *

Quanto melhor o algoritmo de hash que usamos para calcular códigos de hash, melhor será o desempenho das tabelas de hash.

Vamos dar uma olhada em uma implementação "padrão" que usa dois números primos para adicionar ainda mais exclusividade aos códigos de hash computados:

@Override
public int hashCode() {
    int hash = 7;
    hash = 31* hash + (int) id;
    hash = 31 *hash + (name == null ? 0 : name.hashCode());
    hash = 31* hash + (email == null ? 0 : email.hashCode());
    return hash;
}

Embora seja essencial entender as funções que os métodos _hashCode () _ e _equals () _ desempenham, não precisamos implementá-los do zero sempre, pois a maioria dos IDEs pode gerar implementações personalizadas _hashCode () _ e _equals () _ e desde o Java 7, obtivemos um método utilitário _Objects.hash () _ para um hash confortável:

Objects.hash(name, email)

IntelliJ IDEA gera a seguinte implementação:

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 *result + name.hashCode();
    result = 31* result + email.hashCode();
    return result;
}

E Eclipse produz este:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime *result + ((email == null) ? 0 : email.hashCode());
    result = prime* result + (int) (id ^ (id >>> 32));
    result = prime *result + ((name == null) ? 0 : name.hashCode());
    return result;
}

Além das implementações acima hashCode () _ baseadas em IDE, também é possível gerar automaticamente uma implementação eficiente, por exemplo, usando Lombok. Nesse caso, a dependência lombok-maven deve ser adicionada ao _pom.xml:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-maven</artifactId>
    <version>1.16.18.0</version>
    <type>pom</type>
</dependency>

Agora é suficiente anotar a classe User com _ @ EqualsAndHashCode_:

@EqualsAndHashCode
public class User {
   //fields and methods here
}

Da mesma forma, se queremos que classe _HashCodeBuilder_Apache Commons Lang gere um _hashCode () _ Para nossa implementação, a commons-lang A dependência do Maven deve ser incluída no arquivo pom:

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

E _hashCode () _ pode ser implementado assim:

public class User {
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
        append(id).
        append(name).
        append(email).
        toHashCode();
    }
}

Em geral, não existe uma receita universal a ser adotada quando se trata de implementar _hashCode () _. É altamente recomendável ler Java Efetivo de Josua Bloch, que fornece uma lista de https://es.slideshare.net/MukkamalaKamal/joshua-bloch-effect-java-chapter-3 [diretrizes completas] para implementar algoritmos de hash eficientes.

O que se pode notar aqui é que todas essas implementações utilizam o número 31 de alguma forma - isso ocorre porque o 31 possui uma boa propriedade - sua multiplicação pode ser substituída por uma mudança bit a bit mais rápida que a multiplicação padrão:

31* i == (i << 5) - i

*7. Como lidar com colisões de hash *

O comportamento intrínseco das tabelas de hash levanta um aspecto relevante dessas estruturas de dados: mesmo com um algoritmo de hash eficiente, dois ou mais objetos podem ter o mesmo código de hash, mesmo que sejam desiguais. Portanto, seus códigos de hash apontariam para o mesmo bucket, mesmo que tivessem chaves de tabela de hash diferentes.

Essa situação é comumente conhecida como colisão de hash e existem várias metodologias para lidar com isso, com cada uma delas tendo suas prós e contras. O HashMap de Java usa o método de encadeamento separado para lidar com colisões:

*“Quando dois ou mais objetos apontam para o mesmo bucket, eles são simplesmente armazenados em uma lista vinculada. Nesse caso, a tabela de hash é uma matriz de listas vinculadas, e cada objeto com o mesmo hash é anexado à lista vinculada no índice de bucket na matriz.*
*Na pior das hipóteses, vários buckets teriam uma lista vinculada vinculada a ela, e a recuperação de um objeto na lista seria realizada linearmente* . ”

As metodologias de colisão de hash mostram em poucas palavras por que é tão importante implementar _hashCode () _ eficientemente .

O Java 8 trouxe um enhancement interessante para a implementação HashMap - se o tamanho do bucket ultrapassar o limite, a lista vinculada será substituída por um mapa da árvore. Isso permite obter O ( logn _) _ procurar em vez de pessimista _O (n) _.

*8. Criando um aplicativo trivial *

Para testar a funcionalidade de uma implementação hashCode () _ padrão, vamos criar um aplicativo Java simples que adicione alguns objetos _User a um HashMap e use o link:/slf4j-with-log4j2-logback [SLF4J] para registrar uma mensagem no console cada hora em que o método é chamado.

Aqui está o ponto de entrada do aplicativo de amostra:

public class Application {

    public static void main(String[] args) {
        Map<User, User> users = new HashMap<>();
        User user1 = new User(1L, "John", "[email protected]");
        User user2 = new User(2L, "Jennifer", "[email protected]");
        User user3 = new User(3L, "Mary", "[email protected]");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

E esta é a implementação _hashCode () _:

public class User {

   //...

    public int hashCode() {
        int hash = 7;
        hash = 31* hash + (int) id;
        hash = 31 *hash + (name == null ? 0 : name.hashCode());
        hash = 31* hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

O único detalhe que vale a pena enfatizar aqui é que sempre que um objeto é armazenado no mapa de hash e verificado com o método _containsKey () _, _hashCode () _ é chamado e o código de hash computado é impresso no console:

[main] INFO com..entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com..entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com..entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com..entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection

9. Conclusão

É claro que a produção de implementações eficientes _hashCode () _ geralmente requer uma mistura de alguns conceitos matemáticos (ou seja, números primos e arbitrários), operações matemáticas lógicas e básicas.

Independentemente disso, é inteiramente possível implementar _hashCode () _ efetivamente sem recorrer a essas técnicas, desde que tenhamos certeza de que o algoritmo de hash produz códigos de hash diferentes para objetos desiguais e seja consistente com a implementação de _equals () _.

Como sempre, todos os exemplos de código mostrados neste artigo estão disponíveis over no GitHub.