Métodos hashCode() e equals() - Parte 1
Quem são os seus objetos?
Uma das features básicas existentes no java que costuma confundir as pessoas no início, é o par de métodos utilizados para 'identificar' objetos: hashCode() e equals().
Para falar desses dois, primeiro precisamos falar das referências, pois existe um critério de igualdade que precisaremos abordar. Na programação, o critério de igualdade não é rigidamente definido. Em outras palavras, a maneira de verificar se um objeto de uma classe A é igual a outro, muitas vezes é diferente da maneira como determinamos se um objeto da classe B é igual a outro. No geral, para primitivos a igualdade é mais clara. É razoável imaginarmos que 10 == 10 retorne true. No entanto, pode ser surpreendente para o iniciante que:
String a = "TESTE";
String b = new String("TESTE");
System.out.println("TESTE" == a); // imprime true.
System.out.println("TESTE" == b); // imprime false.
System.out.println(a == b); // imprime false.
No entanto, todas as chamadas abaixo imprimem true:
System.out.println("TESTE".equals(a));
System.out.println("TESTE".equals(b));
System.out.println(a.equals(b));
System.out.println(a.equals("TESTE"));
System.out.println(b.equals(a));
System.out.println(b.equals("TESTE"));
Obs.: “TESTE” é um literal de String. Isso quer dizer que esse cara é um objeto do tipo String como qualquer outro mas a sua instanciação é feita pelo compilador, pois é como se ele fosse um objeto que existe no fonte, o que obviamente não é verdade. Se você não entende muito bem isso, não se preocupe. Falarei disso futuramente. Por hora, basta compreender que, como objeto, é tão válida a chamada "TESTE".equals(a) quanto a.equals("TESTE").
Sem entrar no modelo de memória java para strings, que tem as suas peculiaridades (a exploração desse tema fica para um outro post), no início pode parecer intimidador que objetos possam ter um comportamento que parece inconsistente. No entanto, os resultados acima estão corretos e espera-se da JVM que apresente esse comportamento e não outro.
Operador ==
Ao compararmos os objetos String acima, utilizamos duas maneiras: Através do operador == e através do método equals(). O operador == compara o número da referência dos objetos (por isso falei de referências). Sendo uma comparação de números, temos aqui precisão aritmética. Mas, o mais importante a guardarmos é que, ao fazer uma comparação com o operador == estamos perguntando à JVM: essas duas variáveis apontam para o mesmo objeto na memória?
Método equals()
O método equals() por sua vez, pode implementar qualquer critério de igualdade inclusive o mesmo que o operador ==. Através dele, não ficamos mais presos a uma comparação que nos diga se estamos lidando com o mesmo objeto na memória. Isso é importante para muitas situações práticas. Consideremos uma classe hipotética:
/**
* Essa classe descreve um Algo que tem
* um nome não nulo que não pode mudar,
* e que tem obrigatoriamente que ser
* fornecido quando o objeto for construído.
*/
public class Algo extends Object {
private final String nome;
/**
* Se o nome fornecido for nulo, não posso ser construído.
*
* @throws IllegalArgumentException
*/
public Algo(String nome){
if(nome == null){
throw new IllegalArgumentException(
"Algos têm que ter um nome não nulo.");
}
this.nome = nome;
}
public String getNome(){
return this.nome;
}
}
Ocorre que, no meu sistema hipotético eu guardo os Algos no banco de dados para serem usados depois. Assim, quando o usuário preenche o formulário com o nome do Algo, eu tenho que ser capaz de verificar se já existe um Algo igual na base de dados. Um Algo será criado a partir do formulário e outro Algo virá do banco de dados. Nesse caso, estamos claramente falando de objetos diferentes, em endereços diferentes na memória. O operador == vai retornar false se eu comparar esses dois objetos. Nesse momento eu preciso de um critério diferente para determinar a igualdade. Nesse caso, o que parece mais razoável é que devo considerar iguais, Algos que tenha o mesmo nome, ao invés de verificar se estamos falando do mesmo Algo na memória. Abaixo, uma versão do Algo com um equals() gerado pelo eclipse. Reparem que uma simples comparação de uma única String em um objeto pode ser mais sutil do que se imagina no início:
/**
* Essa classe descreve um Algo que tem
* um nome não nulo que não pode mudar,
* e que tem obrigatoriamente que ser
* fornecido quando o objeto for construído.
*
* Objetos dessa classe serão considerados
* iguais se tiverem o mesmo nome.
*/
public class Algo extends Object {
private final String nome;
/**
* Se o nome fornecido for nulo, não posso ser construído.
*
* @throws IllegalArgumentException
*/
public Algo(String nome){
if(nome == null){
throw new IllegalArgumentException(
"Algos têm que ter um nome não nulo.");
}
this.nome = nome;
}
public String getNome(){
return this.nome;
}
@Override
public boolean equals(Object obj) {
// Se for o mesmo objeto, evidentemente terá o mesmo nome.
if (this == obj){
return true;
}
// Se o outro for nulo, evidentemente é diferente de mim.
if (obj == null){
return false;
}
// ATENÇÂO: Neste caso, só posso ser
// comparado com outros Algos. Isso nem sempre
// será verdade. Ex.: Situações onde é razoável
// comparar objetos de subclasses.
if (getClass() != obj.getClass()) {
return false;
}
// Aqui já sei que o outro não é nulo nem de
// outra classe. Posso fazer o cast sem problemas.
Algo other = (Algo) obj;
// Aqui, finalmente verificarei se o outro tem o
// mesmo nome que eu. Observe que não
// verifico se o nome dele está no mesmo lugar
// da memória que o meu (==), mas se
// o nome dele é formado pelos mesmos
// caracteres que o meu.
// ATENÇÂO: Teste de nulidade sobre os nomes
// removido, porque sei que o meu construtor
// só permite nomes não nulos.
if (!this.nome.equals(other.nome)){
return false;
}
return true;
}
}
A partir dessa implementação, podemos fazer comparações entre Algos vindos de lugares diferentes pois poderemos perguntar:
if (algo1 != null) {
boolean b = algo1.equals(algo2);
}
Obs.: Alguem poderia perguntar: não posso pegar a String vinda do usuário e fazer algo como stringDoUsuario.equals(algo.getNome())? Sintaticamente pode, mas o nome disso é gambiarra. Algo não é String, nem caso específico de String, nem tem nenhum relacionamento semântico que faça essa comparação ter sentido. Portanto, evite ao máximo recorrer a esse tipo de solução 'fácil'. Essas coisas costumam voltar como assombrações no futuro ;).
Agora cabe explicar de onde vem esse tal de equals(). Esse método é declarado na classe Object, que é a superclasse implícita de qualquer classe java. Se você não colocar 'extends Object' na declaração da sua classe o compilador vai colocar para você, a menos que a sua classe estenda outra. Se você colocar 'extends Object' na sua classe que a princípio não estendia nenhuma, vai compilar sem problemas. Teste. A moral da história é que todos dos objetos em java têm, inicialmente, implementações válidas de equals() e hashCode() (além das de alguns outros), que são as que vêm de Object. Você só pode herdar uma implementação inválida de um desses métodos se uma superclasse que não Object sobrescrevê-los de forma inválida. A declaração de equals() em Object é a seguinte:
public boolean equals(Object obj) {
return (this == obj);
}
Reparem que em Object, equals() faz uma comparação plana das referências dos objetos envolvidos. Então, a princípio, no mínimo podemos verificar se duas variáveis apontam para o mesmo objeto em qualquer classe java.
Utilizei o método equals() em strings acima. String sobrescreve equals() para comparar se os dois objetos, além de serem strings, são formados pela mesma cadeia de caracteres. O importante a perceber aqui é que comparar a cadeia de caracteres de duas strings para saber se são iguais faz sentido para se determinar se duas strings são iguais. Para tipos de objetos diferentes, podemos vir a comparar números, combinados ou não com strings, ou mesmo outros objetos que sejam atributos daquele que estamos comparando, sabendo que esses atributos terão seus próprios critérios de igualdade. Em suma, o critério de igualdade pode ser qualquer, e fundamentalmente só pode ser determinado pelas necessidades de projeto da sua classe.
Assim, uma implementação adequada de equals(), além de lidar corretamente com os atributos envolvidos, verificando nulidade, classe do objeto em questão etc., deve, em primeiro lugar, definir um critério de igualdade que faça sentido do ponto de vista semântico, para distinguir objetos considerados iguais dentro das necessidades do projeto, daqueles que são diferentes entre si.