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 oScanner
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:
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. :)
Olá,
o que você achou deste conteúdo? Conte nos comentários.