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.



CollectionUtils e Filter<T> Manipulando e criando buscas.

A algum tempo atrás eu andava com uma dúvida de como realizar buscas genéricas em uma Collection tipada, ate que um colega Sérgio Taborda me sugeriu o uso de uma interface muito simples.

A ideia é muito simples cria-se uma interface chamada Filter<T> que é um contrato, que relata como deve ser o objeto que queremos encontrar, e então cria-se rotinas simples baseando neste contrato (a interface) para buscas.

interface Filter<T>

  1. /**
  2.  * Filtro de objeto, que testa se um obejto candidato T confere com o filtro.
  3.  * @author Tomaz Lavieri
  4.  * @param <T> o tipo de objetos que o filtro testa.
  5.  * @see CollectionUtils
  6.  */
  7. public interface Filter<T> {
  8.     /**
  9.      * Verifica se o objeto candidato passa pelo filtro.
  10.      * @param candidate Objeto candidato.
  11.      * @return  <tt>true</tt> - caso o candidato passe no filtro.
  12.      *          <br><tt>false</tt> - caso o candidato não pesse pelo filtro.
  13.      */
  14.     public boolean match(T candidate);
  15. }


É com essa ideia do filtro, que se cria um critério para buscas em coleções, por exemplos, vamo supor que agente tem uma classe chamada Pessoa e que queremos buscar todas as pessoas que contenham o nome "João" e que a idade seja maior ou igual a 18 anos.

  1. Filter<Pessoa> joaoMaioresDe18 = new Filter<Pessoa>() {
  2.     public boolean match(Pessoa candidate) {
  3.         boolean nomeOk = candidate.getNome() != null ?
  4.             candidate.getNome().toLowerCase().contains("joão") :
  5.             false;
  6.  
  7.         return nomeOk && candidate.getIdade() >= 18;
  8.     }
  9. };


Agora temos um critério de busca que foi guardado na variável joaoMaioresDe18 e podemos checar facilmente se qualquer pessoa passa pelo criterio utilizando joaoMaioresDe18.match(pessoa); este retorna true caso a pessoa seja um joão maior de 18 anos.

Agora podemos criar uma rotina para buscar em uma lista, todas as pessoas que pertençam ao criterio informado, ficando assim:

  1. List<Pessoa> pessoas = getAllPessoas(); //abstraindo como essa lista é obtida.
  2. List<Pessoa> resultado = new ArrayList<Pessoa>(0); //guarda o resultado
  3. for (Pessoa pessoa : pessoas) //realizando a busca
  4.     if (joaoMaioresDe18.match(pessoa))
  5.         resultado.add(pessoa);


Após isto, temos em resultado, todas as pessoas que contém o nome "joão" e que são maiores de idade.

Ficou bom, mais o processo pode ser automatizado e tornar-se ainda melhor, para isso é criado uma classe utilitária chamada CollectionUtils onde podemos guardar rotinas como:
  1. Buscar todos os resultados que coincidem com o filtro - findAllMatch
  2. Buscar todos os resultados que não coincidem com o filtro - findAllNotMatch
  3. Buscar o primeiro resultado que coincidir com o filtro - findFirstMatch
  4. Buscar o primeiro resultado que não coincir com o filtro - findFirstNotMatch
  5. Reter na lista total apenas os objetos que coincidirem com o filtro - retainAll
  6. Remover da lista total todos os objetos que coincidirem com o filtro - removeAll
  7. Adciona a uma segunda lista todos os itens da primeira lista que coincidirem com o filtro - addAllMatch
  8. Adciona a uma segunda lista todos os itens da primeira lista que não coincidirem com o filtro - addAllNotMatch


Colocarei aqui apenas um trecho de CollectionUtils e o link para o código completo segue aqui http://pastebin.com/f9d80cb7

class CollectionUtils
  1. import java.util.ArrayList;
  2. import java.util.Collection;
  3. import java.util.Iterator;
  4. import java.util.List;
  5.  
  6. /**
  7.  * Classe utilitária para manipular coleções
  8.  * @author Tomaz Lavieri
  9.  */
  10. public class CollectionUtils {
  11.     /**
  12.      * Não é possivel instaciar CollectionUtils.
  13.      */
  14.     private CollectionUtils(){}
  15.  
  16.     /**
  17.      * Busca todos os objetos <b>T</b> na lista de <tt>candidates</tt> que
  18.      * conferem com o <tt>filter</tt>.
  19.      * <BR>
  20.      * <BR>Tem o resultado inverso a {@link #findAllNotMatch(Collection, Filter)}
  21.      * @param   <T> o tipo de objeto a ser buscado.
  22.      * @param   candidates a coleção de condidatos aonde deseja-se realizar a
  23.      *          busca.
  24.      * @param   filter o filtro que será usado para <b><u>aceitar</u></b> um
  25.      *          candidato no resultado da busca.
  26.      * @return  todos os itens que passaram pelo filtro.
  27.      * @see #findAllNotMatch(Collection, Filter)
  28.      */
  29.     public static <T> List<T> findAllMatch(Collection<? extends T> candidates,
  30.             Filter<T> filter) {
  31.         List<T> matchs = new ArrayList<T>(0);
  32.         addAllMatch(candidates, filter, matchs);
  33.         return matchs;
  34.     }
  35.  
  36.     /**
  37.      * Adciona a coleção <tt>recipient</tt> todos os objetos <tt>T</tt> da lista
  38.      * de <tt>candidates</tt> que coincidirem com o <tt>filter</tt>.
  39.      * <BR>
  40.      * <BR>Adciona o inverso dos itens de <tt>candidates</tt> que
  41.      * {@link #addAllNotMatch(Collection, Filter, Collection)} adcionaria.
  42.      * @param   <T> o tipo de objeto a ser adcionado.
  43.      * @param   candidates a coleção de candidatos a serem adcionados ao
  44.      *          <tt>recipient</tt>.
  45.      * @param   filter o filtro que será usado para <b><u>aceitar</u></b> um
  46.      *          candidato a ser adcionado ao <tt>recipient</tt>.
  47.      * @param   recipient coleção onde os objetos serão adicionados.
  48.      * @see #addAllNotMatch(Collection, Filter, Collection)
  49.      */
  50.     public static <T> void addAllMatch(Collection<? extends T> candidates,
  51.             Filter<T> filter, Collection recipient) {
  52.         for (T object : candidates)
  53.             if (filter.match(object))
  54.                 recipient.add(object);
  55.     }
  56.    
  57. //... restante da classe segue no link indicado
  58.  
  59. }


Agora com essas duas classes, basta criar um filtro, e mandar junto com a coleção, para CollectionUtils e receber os resultados.

Desta forma espera-se aumentar a produtividade, e facilitar a manipulação de coleções.



Filter.java
CollectionUtils.java


Seguidores