Java e OO

Entendendo o equals e hashCode

Será que bugs e performance pode ter haver com os métodos equals() e hashCode()?

Entender os motivos por trás destes métodos nas classes Java fazem muita diferença para criar um software sem bugs e com melhor performance, principalmente quando se trabalha com coleções como ArrayList e HashSet.

Neste artigo você vai aprender sobre os métodos equals() e hashCode(). Entender o que são os códigos hash, como é o funcionamento de uma busca em um HashSet e o que pode acontecer quando não os implementamos corretamente.

Com apenas uma entidade Produto, você vai aprender muito sobre estes métodos. Vamos criar a classe abaixo:

public class Produto {

  private String sku;
  private String nome;

  public Produto(String sku, String nome) {
    this.sku = sku;
    this.nome = nome;
  }

  // getters e setters

  @Override
  public String toString() {
    return "Produto [sku=" + sku + ", nome=" + nome + "]";
  }

}

Os atributos da classe são sku e nome. O sku é um código que identifica unicamente o produto. Normalmente as empresas podem criar uma convenção para ele, e é o que nós vamos fazer também.

Por exemplo, vamos cadastrar os produtos para uma loja que vende impressoras e cartuchos. O sku para uma impressora HP Deskjet modelo 2360 poderia ser: IHPD2360. Repare que criei uma convenção, I de impressora, HP para a marca, D de deskjet e 2360 para o modelo.

Também sobrescrevi o método toString() para facilitar a impressão dos objetos desse tipo, será impresso o sku e o nome.

Agora vamos pensar em uma situação simples, mas que pode facilmente existir em um sistema real, do seu dia a dia.

A ideia é cadastrar produtos armazenando em uma coleção. Irei mostrar em um método main recebendo a entrada de dados do usuário através do console.

Temos a seguinte regra básica: não podem ser adicionados na coleção dois produtos com o mesmo SKU. Será que isso é simples de fazer? Sim, mas é necessário saber o que fazem os métodos equals() e hashCode().

Vamos criar a classe CadastradorProdutos com o código abaixo:

public class CadastradorProdutos {

  public static void main(String[] args) {
    Collection produtos = new ArrayList<>();
    
    System.out.println("##### Cadastro de produtos #####\n");
    
    try (Scanner entrada = new Scanner(System.in)) {
      String continuar = "s";
      while ("s".equalsIgnoreCase(continuar)) {
        System.out.print("SKU: ");
        String sku = entrada.nextLine();

        System.out.print("Nome: ");
        String nome = entrada.nextLine();
        
        Produto produto = new Produto(sku, nome);
        if (produtos.contains(produto)) {
          System.err.println("Esse produto já foi adicionado. Utilize outro SKU!");
        } else {
          produtos.add(produto);
          System.out.println("Produto adicionado.");
        }
        
        System.out.print("Deseja adicionar mais algum produto? (s/n) ");
        
        continuar = entrada.nextLine();
      }
    }
    
    produtos.forEach(System.out::println);

    System.out.println("Fim");
  }

}

O funcionamento é bem simples, mas vale a pena destacar duas partes:

  • O try com o Scanner sendo criado entre os parênteses
  • A impressão dos produtos da coleção com: produtos.forEach(System.out::println)

No primeiro caso usei um recurso do Java 7, chamado de try-with-resources, que ao final do try irá chamar o método close() do Scanner, nos ajudando a não esquecer de invocar esse método.

Já na segunda parte de destaque, usei um novo método e recurso do Java 8. O método forEach() foi adicionado para facilitar a iteração nos elementos da coleção. Cada um dos elementos é entregue ao método println() de System.out, esse recurso é chamado de method reference.

Voltando ao assunto principal do post, nesse momento, se você executar esse código, o que acha que irá acontecer se tentarmos adicionar dois produtos com o mesmo SKU?

Esses produtos serão adicionados, pois o método contains() está retornando false sempre! Isso porque a coleção não sabe pesquisar por sku.

A instância da coleção que usamos foi de ArrayList, e isso quer dizer que ela utilizará o equals() para verificar se a lista contém o objeto. Como esse método não está sobrescrito, o método contains() irá retornar true apenas quando for o mesmo objeto, a mesma instância!

Para ensinar a classe Produto a comparar dois objetos com o sku, vamos sobrescrever o método equals() com o código abaixo:

public boolean equals(Object obj) {
  Produto outro = (Produto) obj;
  return this.sku.equals(outro.getSku());
}

Nesse código estou comparando o sku do objeto atual, com outro objeto recebido no método equals().

Execute novamente o código e veja que agora, quando tentarmos adicionar um novo produto, com o mesmo sku já cadastrado anteriormente, não irá aceitar, pois o método contains irá retornar true.

Ainda vamos alterar esse código, pois ele não está fazendo algumas verificações importantes. Mais tarde te mostro como fazer isso de uma forma bem simples e rápida. ;)

Vamos fazer uma alteração na nossa classe de teste e usar um HashSet no lugar do ArrayList.

Collection produtos = new HashSet<>();

Se executar o código, mesmo com o equals() implementado, você vai perceber que o método contains() irá retornar false mesmo com SKUs iguais.

Mas por que se o equals() está implementado e comparando os objetos baseado no sku? Porque em uma coleção que se usa código hash é importante primeiro determinar em que “região” esse objeto está.

Essa “região” é um espaço dentro da coleção onde os objetos ficam agrupados por semelhança, facilitando assim os encontrar. Para facilitar o entendimento, vamos analisar a figura abaixo:

Ideia de coleções com Hash

Repare que existem 3 caixas que agrupam nomes que começam com uma letra, “J”, “P” ou “D”. Mas atenção, eu inventei essa regra, poderia ser qualquer outra, como o tamanho da palavra, o importante é agrupar as strings.

E essa é a mesma ideia dos códigos hash. Criar um código que agrupe objetos semelhantes.

Imagine buscar um nome qualquer na figura, por exemplo, “Pedro”, você já iria na caixa que a inicial seja “P” e então compararia os nomes lá dentro. Com a coleções que usam hash é a mesma coisa, se quisermos encontrar um produto qualquer, temos que primeiro determinar o código hash e então olhar dentro dessa “caixa” os objetos com o método equals().

Consegue perceber que uma busca assim é bem mais rápida do que comparar um a um os objetos? :)

Para gerar o código hash em um objeto, precisamos sobrescrever o método hashCode(). Ele irá retornar um inteiro que representa o “código da caixa que ele ficará”.

Vamos criar um código baseado na primeira letra do sku. Veja o método hashCode abaixo:

public int hashCode() {
  return this.sku.charAt(0);
}

Execute novamente o código, irá perceber que dois SKUs não podem mais ser inseridos no HashSet. Atingimos nosso objetivo!

Mas, agrupar pela primeira letra não parece uma boa ideia. Pense bem, se tivermos 1000 produtos que comecem com a letra “I” e 50 que começam com a letra “C”. Percebe como as caixas ficariam desbalanceadas? Uma com muitos objetos e outra com poucos.

Podemos começar a pensar em outros algoritmos para gerar o código hash para nós. Mas alguém já pensou nisso! E a notícia boa é que o Eclipse nos auxilia muito nesse código.

Para isso, vamos apagar os dois métodos, equals() e hashCode() e pedir ao Eclipse gerar para nós. Basta clicar no menu Source e então clicar em Generate hashCode() and equals()…

No menu que aparecer, selecione o sku apenas e clique em OK.

Pronto, agora os objetos do tipo Produto serão comparados usando o SKU, assim como o código hash será gerado a partir dele.

Assista ao vídeo com a explicação passo a passo do exemplo, que você vai conseguir entender muito melhor.

Você já conhecia esses detalhes do equals() e hashCode? Comente sobre o que achou da vídeo aula e artigo. :)

Graduado em Engenharia Elétrica pela Universidade Federal de Uberlândia e detentor das certificações LPIC-1, SCJP e SCWCD.

Olá,

o que você achou deste conteúdo? Conte nos comentários.

Junte-se a mais de 100.000 pessoas

Entre para nossa lista e receba conteúdos exclusivos e com prioridade

Você se Inscreveu com Sucesso!