quinta-feira, 2 de abril de 2009

Igualdade em java - equals() e hashCode()

Um dos maiores problemas que os programadores encontram ao inciar seu trabalho em java é o de entender os testes de igualdade.

A igualdade em java não pode ser testada através do operador == , estes operador apenas verifica se duas variaveis fazem referencia a mesma instancia de um objetos.

Para ser um pouco mais claro, imaginemos uma classe Pessoa, como segue abaixo.

  1. public class Pessoa {
  2.     private String nome;
  3.     private String cpf;
  4.     private Integer idade;
  5.    
  6.     public Pessoa(String nome, String cpf) {
  7.         this.nome = nome;
  8.         this.cpf = cpf;
  9.     }
  10.    
  11.     public Integer getIdade() {
  12.         return idade;
  13.     }
  14.    
  15.     public void setIdade(Integer idade) {
  16.         this.idade = idade;
  17.     }
  18.  
  19.     public String getNome() {
  20.         return nome;
  21.     }
  22.    
  23.     public String getCpf() {
  24.         return cpf;
  25.     }
  26. }


Como posso eu falar que duas pessoas são as mesmas pessoas ? a JVM (Java Virutal Machine) por se só não tem como identificar como duas pessoas podem ser ditas iguais, desta forma o teste abaixo, verifica apenas se estas são instancias de um mesmo objeto.

  1. Pessoa maria = new Pessoa("Maria da Silva","111.222.333-44");
  2. maria.setIdade(15);
  3.  
  4. Pessoa mariaDaSilva = new Pessoa("Maria da Silva","333.555.222-11");
  5. mariaDaSilva.setIdade(40);
  6.  
  7. //apesar dos nomes iguais de maria e mariaDaSilva, os cpfs são
  8. //diferentes e desta forma posso falar que são pessoas diferentes
  9.  
  10. Pessoa mariaDaSilva2 = new Pessoa("Maria da Silva","333.555.222-11");
  11.  
  12. //mariaDaSilva e mariaDaSilva2 são pessoas iguais, e mesmo não setando
  13. //a idade de mariaDaSilva2, em um teste de igualdade eu vou querer que
  14. //para esses 2 objetos ele seja positivo.
  15.  
  16. //porem são 2 instancias de de objetos diferentes, portanto
  17.  
  18. boolean referencia = mariaDaSilva == mariaDaSilva2;
  19.  
  20. System.out.println("Referencia é falso (" + referencia + ")");
  21. //isso ocorre pq:
  22. //a variável mariaDaSilva aponta para um objeto enquanto
  23. //a variável mariaDaSilva2 aponta para outro objeto.


Equals:

Para resolver esse problema em java existe um mecanismo de igualdade que vem em todo objeto, e descende da classe Object, onde vc pode testar a igualdade entre objetos através de: primeiroObjeto.equals(segundoObjeto); este teste, quando não sobrescrito apenas testa se os 2 objetos são referencia de uma mesma instancia, ou seja, tem o mesmo efeito que primeiroObjeto == segundoObjeto;.

Antes de implementar um equals em nossos objetos é preciso saber de cinco regras.
Equals...
  1. É reflexivo: para qualquer qualquer referencia não nula a, a.equals(a) tem sempre que ser verdadeiro, ou seja, um objeto é sempre igual a se mesmo.

  2. É simétrico: para quaisquer referencias não nulas de a e b, a.equals(b) so poderá retornar verdade se b.equals(a) retornar verdade, o mesmo raciocinio vale para falso.

  3. É transitivo: para quisquer referencias não nulas de a,b e c, se a.equals(b) e b.equals(c) são verdade então também tem que ser verdade que a.equals(c).

  4. É consistente - para quaisquer referencias não nulas de a e b, multiplas invocações de a.equals(b) terá sempre que retornar true ou sempre retornar false, enquanto informações usadas na comparação do equals não sejam alteradas.

  5. O resultado de equals() entre um objeto e nulo deve retornar sempre falso, a.equals(null) é sempre falso.


Com essa ideia podemos pensar em como implementar o equals para nossa classe Pessoa, sabemos que, duas Pessoas de mesmo nome podem ser diferente, portanto esse não pode ser nosso parametro para teste. Duas pessoas não podem ter o mesmo número de CPF, então este número é unico, e pode identificar uma pessoa, sendo assim nosso teste de igualdade entre duas pessoa pode ser

  1. @Override
  2. public boolean equals(Object obj) {
  3.     if (obj == null) {
  4.         return false; //nenhum objeto pode ser igual a null
  5.     }
  6.     if (!obj instanceof Pessoa) { //uma pessoa só pode ser igual a outra pessoa
  7.         return false;
  8.     }
  9.     final Pessoa other = (Pessoa) obj;
  10.     if (getCpf() == null) //se não houver CPF não há como testar as duas pessoas.
  11.         return false;
  12.     return getCpf().equals(other.getCpf()); //duas pessoas serão iguais...
  13.                                             //se seus CPFs forem iguais...
  14. }


HashCode:

Assim como há um teste de igualdade, Java ainda dispoem de outro teste, onde é possivel rapidamente, através de integer, restringir o campo de igualdade, através de hashCode(). assim como equals, o hashCode segue um contrato descrito abaixo:


  1. É constante: A qualquer momento durante a excecução de um programa java, o método hashCode deve constantemente retornar o mesmo inteiro, enquanto informações usadas na comparação do equals não sejam alteradas. O hashCode não precisa ser constante de uma execução da aplicação para outra.

  2. Se dois objetos a e b, são iguais, a.equals(b) retorna verdadeiro, então os inteiros produzidos por a e b devem ser os mesmos, ou seja a.hashCode() == b.hashCode()

  3. O inverso não é necessariamente verdadeiro, dois objetos que produzirem o mesmo hashCode podem ser diferentes



Diante disso a utilização do hashCode é principalmente para excluir igualdade, ou seja, ele serve como uma peneira, e é largamente utilizado por Collection em buscas como contains, remove, removeAll, retainAll entre outras coisas. A ideia é se os hashCode são diferentes então não é preciso testar a igualdade, pois já se garante que os objetos não serão iguais, poupando processamento no momento da busca, pois hashCode usa um calculo com int, que consome pouco processo, geralmente bem menos que o equals.

Existem varias estrategias para calculo do hashCode, desde a mais simples, onde vc praticamente exclui a funcionalidade do hashCode, deixando seu valor constante para qualquer objeto.

  1. @Override
  2. public int hashCode() {
  3.     return 0;
  4. }


Este método acima, funciona e respeita as regras de contrato de hashCode, porem seu programa poderá perder em velocidade. É extremamente desaconselhavel o uso de hashCode constante para qualquer objeto, pois várias funcionalidade que otimizam processos de busca não terão serventia com um hashCode mau projetado.

A melhor estratégia para hashCode é olhar para equals, e verificar como a igualdade é exegida, e assim montar um hashCode, concetenando os hashCode dos objetos que fazem a igualdade e designando um peso para as propriedades.

Por exemplo, para nossa classe Pessoa uma boa estrategia de hashCode seria

  1. @Override
  2. public int hashCode() {
  3.     int hash = 7;
  4.     hash = 23 * hash + (getCpf() != null ? getCpf().hashCode() : 0);
  5.     return hash;
  6. }


O número 7 contido nesse hashCode é apenas um número randomico para startar o hashCode, e diferenciar de outros objetos, por exemplo, um objeto empresa poderia iniciar por 3. o número 23 é outro número randomico, usado para quando há varias propriedades e escalar o hashCode entre as varias propriedade que definem um objeto igual.

Como no exemplo acima usamos o hashCode da string cpf, e como String tem seu hashCode bem implementando, podemos assim garantir que sempre que os CPF forem iguais os hashCode serão iguais.


Conclusão:

A igualdade entre objetos é uma parte fundamental da programação, e é uma das necessidades basica de logica, saber se dois objetos são iguais.

A implementação destes dois métodos é importante, e deve ser implementado em todas as classes instanciáveis do projeto, de forma a prover mecanismos corretos de comparação de igualdade (equals), assim como uma forma de otimizar os processos de busca (hashCode).

Muitos programadores só começam a entender a real necessidade da correta implementação destes dois métodos ao utilizar mais largamente a Java Collection Framework do pacote java.util, pois varios de seus métodos como contains, removeAll, remove, ratainAll ... falham quando equals não esta corretamente implementado, e perdem perfomance ou falham quando hashCode não esta otimizado ou corretamente implementando.



5 comentários:

  1. Olá Tomaz,

    Cara, legal esse seu post, demonstra toda a importância do equals e hashCode, pois é muito comum pegarmos cenários em que um sistema grande tem seus objetos de domínio com o equals/hashCode mal implementados, fazendo com que o uso das collections se torne um trabalho muito complicado. Se as regras de implementação fossem sempre seguidas, a manutenção seria bem mais simples.

    Parabéns pelo post, abraços!

    ResponderExcluir
  2. Opa Eduardo,

    Obrigado pelos comentários, essa é a intenção, ajudar o pessoal a conhecer a importancia e as vantagens de ter o equals/hashCode bem implementados.

    É bastante rotineiro ver no forum que nós frequentamos o GUJ, varias pessoas reclamando que funcionalidade de suas List e Set estão com problemas, sem saber o real motivo, espero com este artigo ajudar mais gente no entendimento, pois em um replay de forum, uma explicação mais detalhada é complicado.

    Um abraço, espero que continue lendo

    ResponderExcluir
  3. Cara vou lhe corrigir, ehhehehe.
    Bacana o post, mas seu metodo equals ta errado.

    vc faz a verificação
    if(obj instanceof Pessoa){
    return false
    }
    Ou seja se estiver comparada com um pessoa vai retornar false.
    o correto seria
    if(!obj instanceof Pessoa){
    return false
    }

    ResponderExcluir
  4. Obrigado rafael ^^ , erro de digitação acontece, realmente tava faltando o ! o que fazia o código falhar ^^ é realmente se obj NÃO for instnaceof pessoa...

    obrigado a ajuda, um abraço

    ResponderExcluir
  5. Ola... como eu faço para remover um objeto de um ArrayList do tipo Object[]?
    Tem como passar um comparator?
    Ele usa comparação por referência?

    ResponderExcluir

Seguidores