Blog sobre desenvolvimento de software (Java, muito Java!), inovação tecnológica e cotidiano do Universo de TI. Acesse notícias, tutoriais, material de cursos e eventos, código, desafios, soluções, opiniões, pensamentos, divagações, balbuciações e abobrinhas diversas. Deixe seu comentário!

domingo, 8 de maio de 2011

Java: == vs equals()

Operadores e métodos de comparação são fonte de muitas dúvidas para programadores que estão iniciando em Java. De fato, o operador  ==, os métodos equals e hashCode são tópicos do Java Standard Edition 6 Programmer Certified Professional Exam para quem busca o título Oracle Certified Professional Java Programmer (antigo SCJP). Várias questões cobram, direta ou indiretamente, conhecimento sobre o assunto.
O operador == é usado para comparação entre tipos primitivos:
int x = 10;
if (x == 10) {
  System.out.println("x é 10!");
}
Já para tipos complexos o programador pode se surpreender com o resultado. O operador == só "funciona" com referências para o mesmo objeto:
public class Pessoa {
    
    private String cpf;

    public Pessoa(String cpf) {
        this.cpf = cpf;
    }
    
    public static void main(String[] args) {
        Pessoa p1 = new Pessoa("111.111.111-11");
        Pessoa p2 = new Pessoa("111.111.111-11");
        Pessoa p3 = p1;
        
        System.out.println(p1 == p2); // false
        System.out.println(p1 == p3); // true
    }
    
}
Esse resultado remete a uma decisão de design da linguagem. A regra de negócios para determinar se dois objetos são iguais é específica para cada aplicação. Enquanto o compilador pode assumir que duas referências para o mesmo objeto são iguais, assumir que dois objetos diferentes com os mesmos valores são iguais seria perigoso. Só porque uma pessoa tem um carro do mesmo modelo e cor do que o seu, isso não significa que os carros são iguais e você pode pegar o carro dela.
Eis o motivo de existência do método equals na classe Object. Para quem não está familiarizado com essa classe, ela serve de superclasse para todas as classes em Java, logo, seus métodos são universais, podendo ser chamados a partir de qualquer instância de qualquer objeto disponível em tempo de execução. A fim de definir a regra de comparação para objetos de uma classe deve-se sobrescrever o método equals no corpo da mesma: 
public class Pessoa {
    
    private String cpf;

    public Pessoa(String cpf) {
        this.cpf = cpf;
    }

    // Adaptado do código gerado pelo Netbeans
    @Override
    public boolean equals(Object obj) {
        // Um objeto é sempre igual a ele mesmo
        if (this == obj) {
            return true; 
        }
        // Um objeto nunca deve ser igual a null
        if (obj == null) {
            return false;
        }
        /* Uma pessoa só pode ser igual a outra pessoa.
         * Caso uma pessoa possa ser igual a uma de suas subclasses
         * use !(obj instanceof Pessoa)
         */
        if (getClass() != obj.getClass()) {
            return false;
        }
        // Converte a referencia para uma pessoa
        final Pessoa other = (Pessoa) obj;
        // Duas pessoas só podem ser comparadas se possuem CPF
        if (this.cpf == null || other.cpf == null) {
            return false;
        }        
        // Duas pessoas são iguais se possuem o mesmo CPF
        return this.cpf.equals(other.cpf);
    }
    
    public static void main(String[] args) {
        Pessoa p1 = new Pessoa("111.111.111-11");
        Pessoa p2 = new Pessoa("111.111.111-11");
        Pessoa p3 = p1;
        
        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.equals(p3)); // true
    }
    
}
Agora sim... Veja que no meu exemplo duas pessoas só são iguais se possuem o mesmo CPF. Outros sistemas poderiam adotar critérios diferentes para comparar duas pessoas, ou mesmo comparar pessoas com outros objetos (do tipo Batatinha por exemplo). Observe que a comparação entre CPFs também foi feita através de equals; isso é possível pois a classe String (API padrão do Java) sobrescreve o método equals (o mesmo é valido para várias outras classes como Integer, Long, Boolean, etc).
Quem acompanhou até aqui deve estar se perguntando o que acontece se houver uma chamada para equals sem que seja feita a sobrescrita do método. Não passe vontade! Vá lá, comente o método equals e rode a aplicação. Eu espero...
Pronto? Eis o resultado:
System.out.println(p1.equals(p2)); // false
System.out.println(p1.equals(p3)); // true
Acontece a mesma coisa do que se utilizássemos o operador ==, ou seja, os objetos são comparados por referência. Para os curiosos, segue a implementação no método equals na classe Object que justifica o porquê:
public boolean equals(Object obj) {
   return (this == obj);
}
Uma regra que exige certo grau de disciplina do desenvolvedor é sempre sobrescrever o método hashCode junto com o método equals. Se essa regra não for respeitada, várias classes do Java que dependem da função hash do objeto apresentarão comportamento inconsistente para o mesmo (exemplos dessas classes são as collections HashSet e HashMap). 
O que é uma função hash e para que ela é utilizada fico devendo para um próximo post. Por enquanto assuma que o método hashCode deve mapear um objeto qualquer para um número inteiro para fins de comparação.
Lendo o javadoc do método hashCode, um ponto importante do contrato que deve ser respeitado é: Se a comparação entre dois objetos com equals retornar true os objetos devem possuir o mesmo hashCode (o inverso não é verdadeiro, ou seja, dois objetos com o mesmo hashCode não necessariamente precisam retornar true para a função equals). Segue uma implementação gerada pelo Netbeans que atende a esse contrato para a classe Pessoa:  
@Override
public int hashCode() {
   int hash = 5;
   hash = 37 * hash + (this.cpf != null ? this.cpf.hashCode() : 0);
   return hash;
}
Observe que:
  1. Toda pessoa com o mesmo CPF possui o mesmo hashCode.
  2. Como para o método equals, String e várias outras classes do Java sobrescrevem o método hashCode, sendo possível reaproveitá-lo em nossas implementações.
Cuidado: O uso incorreto do operador == com objetos complexos pode introduzir bugs difíceis de serem rastreados. Veja o seguinte exemplo com strings:
String s1 = "abacate";
String s2 = "abacate";
String s3 = new String("abacate");

System.out.println(s1 == "abacate"); // true
System.out.println(s1 == s2); // true 
System.out.println(s1 == s3); // false
Nesse caso s1 e s2 foram criados a partir do mesmo literal “abacate”. A JVM, por motivos de performance / eficiência no consumo de memória fez com que somente uma String contendo o valor “abacate” fosse de fato criada e ambas as referências apontassem para ela (através de um pool de strings). Já em s3 houve a criação de um objeto diferente em memória (com new), logo, o operador == irá retornar false.
Sempre é possível usar o método intern para obter o objeto String presente no pool, apto a ser comparado com o operador ==. Porém, esse é o típico caso em que você deve se perguntar qual a vantagem de fazer isso (e se o motivo não for convincente, simplesmente não faça).

Resumindo: 
  • Use == para primitivos.
  • Use equals para tipos complexos.
  • Verifique sempre se as classes dos objetos que você está tentando comparar sobrescrevem equals corretamente.
  • Se não sobrescreverem, sobrescreva equals de acordo com sua regra de negócio.
  • Se você sobrescrever equals deverá também sobrescrever hashCode.
Dica: Gerando os métodos equals e hashCode com seu IDE favorito:
  • Netbeans: Alt + Insert  equals() and hashCode() 
  • Eclipse: Menu Superior  Source  Generate hashCode() and equals()
  • IntelliJ: Alt + Insert  equals() and hashCode()

Curiosidade: Veja que os IDES produzem implementações diferentes, porém válidas para ambos os métodos.


UPDATE: A pergunta Comportamento das diferentes formas de comparação em Java no Stack Overflow em Português está levantando algumas questões bem interessantes sobre esse assunto. Vale a pena conferir.

Nenhum comentário: