Para que servem os métodos ToString(), Equals() e GetHashCode()?

Muitas pessoas costumam se confundir com três métodos que qualquer classe possui no .NET e no Java: os métodos ToString(), Equals() e GetHashCode(). Os dois primeiros são mais ou menos claros até certo ponto, mas o último vive causando confusão e pânico entre desenvolvedores Java e C#, ainda mais na hora em que necessitamos sobrescrevê-los… Que tal entendermos de uma vez por todas para que estes três métodos servem?

Importante: os conceitos que veremos aqui servem tanto para a plataforma Java quanto para a plataforma .NET! 😀

Por que toda classe tem estes métodos?

No Java e no .NET, toda classe tem um ancestral por “padrão”: a classe Object. Dessa maneira, qualquer objeto de qualquer classe que criarmos em Java ou .NET, pelo Princípio da Substituição de Liskov, também será uma instância de Object. E a classe Object já expõe por padrão pelo menos os três métodos que estão em discussão.

O método ToString()

Talvez este seja o método que é mais claro com relação ao seu propósito. Seu objetivo é trazer uma representação textual de uma instância de um objeto.
Essa representação textual de um objeto vem a ser muito útil principalmente em situações de debugging e de logging. Isso ocorre porque os métodos de saída para o streamming padrão (os famosos System.out.print[ln]() ou Console.Write[Line]()), assim como os principais métodos de praticamente todas as APIs de log (métodos como o debug() e info()) sempre chamam por padrão o método ToString() de instâncias de objetos que tenham sido passadas para eles.

O exemplo abaixo está escrito em C#, mas o princípio é exatamente o mesmo para o Java também, rs.

Imagine o código abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Console.WriteLine(p);
// ou
Console.WriteLine(p.ToString());

Perceba que não foi realizada nenhuma sobrecarga do método ToString(), o que faz com que a chamada deste método para a classe Pessoa encaminhe a chamada para a classe ancestral, ou seja: na verdade será chamado o método Object.ToString().

Não se esqueça: ==todo e qualquer objeto/classe sempre vai ter o método ToString() por causa da herança da classe Object==. Se a própria classe não implementa este método, a chamada será encaminhada para o ToString() “padrão”, ou seja, o método ToString() da classe Object.

No caso do C#, a saída do código abaixo seria a representada abaixo, independente da chamada explícita ao método ToString() ou não:

TreinaWeb.Exemplo.Pessoa

A saída tanto para a chamada explícita ao método ToString() como para a chamada ocultando-se a chamada é a mesma. Isso ocorre porque os método Console.Write[Line]() chama o método ToString() da instância repassada como parâmetro por padrão. 😉

O que o .NET emite como saída nesses casos é um elemento conhecido como Full Qualified Name, ou simplesmente FQN. Apesar de o nome assustar, ele é muito simples: trata-se somente do “nome completo” e único da classe, que é composto por namespace + nome da classe.

Se tivéssemos o código equivalente em Java, teríamos uma saída similar à abaixo:

TreinaWeb.Exemplo.Pessoa@1033203

A saída é muito similar ao FQN do .NET, com o acréscimo deste número precedido por @. Costumam dizer por aí que este número logo após o @ é a posição de memória do objeto. Daqui a pouco vamos ver que não é “beeem” assim, hehe.

O grande ponto é que essa saída fornecida pelo método ToString() não tem nenhuma utilidade prática para nós, tanto no streamming padrão de saída, quanto em um arquivo de log. Daí vem a necessidade da sobrescrita deste método.

A idéia é que o método ToString() forneça uma representação simplificada e direta do estado do objeto em questão. Uma maneira de atingir este objetivo é fazer com que a saída forneça o valor atual dos atributos do objeto.

Poderíamos sobrescrever o método ToString() da nossa classe Pessoa da seguinte maneira:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override string ToString()
        {
            return string.Format("Pessoa [Nome = {0}]", this.Nome);
        }
    }
}

Nossa saída agora, tanto em Java quanto em .NET, seria a mesma abaixo:

Pessoa [Nome = TreinaWeb]

Agora temos uma representação “palpável” e que faça sentido para nós. Essa é com certeza uma representação muito melhor do que as representações padrão da classe Object.

O método Equals()

Antes de falarmos do método Equals(), precisamos relembrar um pouco sobre manipulação de memória, principalmente sobre stack e heap.

Se você quer relembrar como funciona a manipulação de memória, tanto em Java quanto em .NET, você pode ver este nosso artigo, onde tratamos sobre manipulação de memória, stack, heap, value-types e reference-types.

Quando criamos um objeto da classe Pessoa, este objeto será armazenado na memória heap:

Pessoa minhaPessoa = new Pessoa();

Alocação de memória: reference-type

Não se esqueça de que o compilador não acessa os objetos na heap de maneira direta por questões de performance. Sendo assim, o acesso a esse objeto armazenado na heap é feito através de uma referência dentro da stack para o objeto minhaPessoa, apontando onde na memória heap que este objeto está de fato guardado! Sim: estamos falando de ponteiros.

Acesso à memória: reference-types

É importante entendermos estes conceitos para entendermos melhor como o método Equals() funciona.

Quando comparamos objetos, é considerada uma boa prática utilizarmos o método Equals() para fazer a comparação de igualdade. E, quando a classe a qual os objetos em questão pertencem não sobrescreve o método Equals(), o método Object.Equals() será chamado.

Vamos verificar o código abaixo, considerando ainda a classe Pessoa:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = new Pessoa { Nome = "TreinaWeb" }; 

Console.WriteLine(p.Equals(p2));

Faria muito sentido se esse código fornecesse como saída true, afinal, os objetos aparentemente são iguais, certo? Mas esse código irá produzir false, apesar de o nome nos dois objetos serem iguais… Por que isso acontece?

Aí entra em cena a relação entre o método Object.Equals() e a maneira como a memória é manipulada no Java e no .NET.

A cada vez que utilizamos a keyword new, nós estamos instruindo o compilador a reservar um espaço na memória heap e criar um ponteiro na stack para que seja possível acessar esta área da heap. Sendo assim, no código acima, são criadas duas áreas de memória na heap distintas. Estas áreas são gerenciadas, respectivamente, pelos ponteiros p e p2.

O que ocorre quando utilizamos o Equals() baseado na implementação de Object.Equals() é que este por padrão verifica se os ponteiros ==apontam para a mesma área de memória na heap!== Como temos dois objetos instanciados de maneira distintas (chamamos o new para cada um dos ponteiros), nós temos também duas posições de memória distintas para cada um dos objetos p e p2. Por isso, por padrão, temos como resposta false para o código acima.

Agora, vamos imaginar o código abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }
    }
}
// ...
Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = p; 

Console.WriteLine(p.Equals(p2));

Agora sim! Perceba que para p2 nós não instruímos o compilador a criar uma nova área de memória na heap, e sim o instruímos a fazer com que p2 aponte para a mesma posição de memória na heap para a qual p aponta. Desta maneira, neste último código, obteremos true como resposta quando utilizamos a implementação do método Equals() baseada em Object.Equals().

Agora, não faz muito sentido para nós esta implementação do método Equals() baseado em Object.Equals(). Para nós, faria muito mais sentido que o método Equals() na primeira situação deste tópico retornasse true, afinal, ambos os objetos Pessoa possuem o mesmo nome. Por isso, é importantíssimo para nós sobrescrevermos de maneira adequada o método Equals().

Antes de partirmos para a sobrecarga, é importante entendermos algumas premissas para o método Equals():

  • Se ambos os objetos que estão sendo comparados apontam para a mesma posição de memória, é mandatório que Equals() retorne true, baseado na implementação de Object.Equals();
  • x.Equals(x) sempre tem que retornar true;
  • x.Equals(y) sempre tem que retornar o mesmo que y.Equals(x). Este princípio é conhecido como princípio de simetria;
  • Se x.Equals(y) e y.Equals(z), z.Equals(x) tem que retornar true também. Isso ocorre por decorrência do princípio da simetria;
  • x.Equals(null) sempre será falso. O método Equals() por definição não pode retornar exceções do tipo NullReferenceException ou NullPointerException. Isso faz sentido: se um ponteiro é nulo, na verdade ele não aponta para nenhuma área da heap e, portanto, é impossível fazer uma comparação coerente entre os objetos.

Tendo todos estes princípios em vista, temos 3 pontos importantes a serem observados quando sobrescrevermos o método Equals() para que este atenda a todos estes requisitos:

  • Precisamos ver se um dos participantes da chamada do método Equals() é nulo;
  • Precisamos ver se os dois objetos apontam para a mesma área de memória;
  • Precisamos comparar o estado interno dos participantes da chamada do Equals().

Poderíamos sobrescrever o método Equals() da nossa classe Pessoa() da seguinte maneira, afim de que a nossa sobrecarga atenda aos requisitos essenciais:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override bool Equals(Object obj)
        {
            // Verificando se o segundo participante está nulo
            if (obj == null)
            {
                return false;
            }
            Pessoa p2 = obj as Pessoa;
            // Verificando se o cast foi realizado com sucesso. 
            // Caso não foi, obj nem é um objeto do tipo Pessoa 
            // e automaticamente o método tem que retornar false
            // NOTA: o operador de cast "as" retorna
            // null caso o cast não seja possível
            if (p2 == null)
            {
                return false;
            }
            // Vamos agora verificar se ambos apontam para a mesma posição 
            // de memória utilizando Object.Equals()
            if (base.Equals(obj))
            {
                return true;
            }
            // Agora comparamos o estado interno dos objetos!
            return this.Nome == p2.Nome;
        }
    }
}

Agora nós temos o método Equals() devidamente sobrescrito e respeitando todas as condições necessárias. Considerando esta sobrecarga, se chamarmos o código abaixo…

Pessoa p = new Pessoa { Nome = "TreinaWeb" };
Pessoa p2 = new Pessoa { Nome = "TreinaWeb" }; 
Pessoa p3 = p2;

Console.WriteLine(p.Equals(p2));
Console.WriteLine(p2.Equals(p3));
Console.WriteLine(p.Equals(p3));

… obteremos true em todas as saídas, o que faz muito sentido!

No caso específico do C#, nós ainda poderíamos utilizar p == p2, o que causaria também a chamada de Object.Equals(). Por isso, no C#, temos a possibilidade de fazermos a sobrescrita também de operadores. Nesta situação, precisaríamos sobrescrever o operador == para a classe Pessoa para que tenhamos tudo “nos trilhos” e não tenhamos duas implementações distintas de igualdade. Da mesma maneira, acaba sendo prudente sobrescrever também o operador !=.
Nosso código poderia ficar como está abaixo:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public static bool operator ==(Pessoa a, Pessoa b)
        {
            // Fazemos o cast para Object
            // para evitar StackOverFlowException:
            // o código cairia em loop infinito
            // chamando de maneira encadeada
            // o método Equals() sobrescrito
            // e o operador == sobrescrito! 😉
            if ((((object)a) == null) || (((object)b) == null))
            {
                return false;
            }
            return object.Equals(a, b) || a.Nome == b.Nome;
        }

        public static bool operator !=(Pessoa a, Pessoa b)
        {
            return !(a == b);
        }

        public override bool Equals(Object obj)
        {
            // Verificando se o segundo participante está nulo
            if (obj == null)
            {
                return false;
            }
            Pessoa p2 = obj as Pessoa;
            // Verificando se o cast foi realizado com sucesso. 
            // Caso não foi, obj nem é um objeto do tipo Pessoa 
            // e automaticamente o método tem que retornar false
            if (p2 == null)
            {
                return false;
            }
            // Vamos agora verificar se ambos apontam 
            // para a mesma posição de memória utilizando Object.Equals()
            if (base.Equals(obj))
            {
                return true;
            }
            // Agora comparamos o estado interno dos objetos!
            return this.Nome == p2.Nome;
        }
    }
}
O método GetHashCode() ou hashCode()

Por fim, temos o famigerado método GetHashCode() ou hashCode(). Este é um dos métodos que causam mais confusão nos desenvolvedores.

O hash code é um número inteiro que é gerado de maneira única para cada objeto que esteja alocado em memória. É como se ele fosse um ID único para cada objeto que esteja sob domínio da CLR ou da JVM.

E onde este código único é utilizado? Aí é a grande sacada! Ele é utilizado principalmente dentro de coleções com a finalidade de melhoria da performance. A JVM e a CLR utilizam o hash code internamente para localizar objetos em coleções de maneira mais rápida. E daí também vem sua relação direta com o método Equals().

Uma das maneiras que os ambientes de execução do .NET e do Java utilizam para ver se um determinado objeto existe dentro de uma coleção é comparando os hash codes dos objetos pertencentes à coleção com o hash code do objeto a ser localizado. E, se um objeto é localizado dentro de uma coleção, é porque ele é igual ao elemento dentro de alguma posição da coleção em questão. Percebe a relação entre GetHashCode() e Equals()?

Por isso, vale a máxima abaixo para o método GetHashCode():

Se x.Equals(y) e x e y são objetos da mesma classe, x.GetHashCode() == y.GetHashCode() tem que obrigatoriamente retornar true.

A sobrescrita do Equals() impacta diretamente na implementação do GetHashCode() e vice-versa por causa dessa relação entre os dois. E é por isso que as IDEs emitem warnings quando você, por exemplo, sobrescreve o método Equals() e não sobrescreve o método GetHashCode() e vice-versa.

Agora, um ponto interessante: nem a CLR e nem a JVM garantem a situação inversa! Isso quer dizer que dois objetos, deste que de tipos (ou classes) diferentes, podem por coincidência retornar o mesmo hash code!

Se x é do tipo A e y é do tipo B (ou seja: x e y são objetos de classes diferentes), x.Equals(y) irá retornar por definição false; porém, pode ser que x.GetHashCode() == y.GetHashCode() retorne true.

Essa situação, apesar de ser incomum, pode acontecer. Ela é chamada de colisão. A colisão, quando ocorre, geralmente acontece por causa de sobrescrita equivocada do método GetHashCode().

O método padrão Object.GetHashCode() tem uma implementação um pouco complexa. Se estivermos falando de .NET, a chamada a Object.GetHashCode() irá ser convertida para uma chamada de baixo nível para ObjectNative::GetHashCode. Se estivermos falando de Java, a chamada de Object.hashCode() irá converter a posição de memória onde o objeto está alocado para uma representação numérica, adotando esta representação como sendo o hash code. Inclusive, lembra-se da implementação padrão do método toString() no Java? Pois então… Aquele número estranho que vem depois do @ é o hash code do objeto! =)

Existe um outro ponto muito importante com relação ao método GetHashCode():

Se GetHashCode() tem relação direta com Equals(), um mesmo objeto sempre deverá retornar o mesmo hash code, da mesma maneira que seu método Equals() sempre retornará o mesmo resultado quando haver uma comparação entre dois objetos.

Aí entra um problema grave: a implementação padrão vinda de Object.GetHashCode() não retorna o mesmo hash code para o mesmo objeto. Na verdade, a cada chamada ao método padrão Object.GetHashCode(), um novo hash code será invocado. Isso faz cair por terra o ganho de performance que a utilização de GetHashCode() poderia trazer… Daí vem a necessidade de sobrescrevermos corretamente este método em nossas classes.

Uma técnica geralmente utilizada para conseguirmos sobrescrever corretamente o método GetHashCode() é somar os hash codes de todos os atributos da classe e multiplicar por um número primo. Isso reduz bastante as chances de haver algum tipo de colisão.

Sendo assim, poderíamos sobrescrever o método GetHashCode() da classe Pessoa da seguinte maneira:

namespace TreinaWeb.Exemplo
{
    public class Pessoa
    {
        public string Nome { get; set; }

        public override int GetHashCode()
        {
            // 17 é um número primo! 😉
            return this.Nome.GetHashCode() * 17;
        }
    }
}

O método GetHashCode() auxilia na performance dentro de coleções porque é muito mais simples para os compiladores comparar dois números inteiros do que chamar o método Equals() para cada elemento que faça parte da coleção, sendo que a implementação do Equals() pode ser um pouco complexa e lenta.

Sendo assim, quando o compilador precisa localizar um objeto dentro de uma coleção, ele faz uma iteração em cada elemento que faça parte e faz a comparação com o objeto a ser localizado da seguinte maneira:

  • A primeira comparação é feita através do hash code dos objetos envolvidos. Se eles forem diferentes, o compilador para o trabalho por aqui, já que se dois objetos são iguais (ou seja, o método Equals() com os dois objetos deveria retornar true), o hash code de ambos também deveria ser igual;
  • Caso os hash codes sejam iguais, o compilador levanta a hipótese de estar havendo uma colisão. Então, ele chama o método Equals() para confirmar se os objetos são iguais ou não. Se eles forem iguais, o compilador considera que encontrou o objeto dentro da coleção. Caso não, o compilador avança para o próximo item da coleção e reinicia o processo de comparação.

Consegue perceber como o hash code pode acelerar o processo de manipulação de coleções? Ele ajuda o compilador a evitar a chamada ao método Equals(), que pode ser lento, de maneira desnecessária! o/

O método GetHashCode() é importantíssmo para coleções. Desde coleções mais básicas, como ArrayList; até coleções mais complexas, como Dictionary<TKey, TValue> ou Map<K, U>, se utilizam deste método. O Dictionary<TKey, TValue>, inclusive, utiliza este método para localizar se existem chaves duplicadas ou não.

Os métodos ToString(), Equals() e GetHashCode() são importantíssimos!

Esperamos que você tenha percebido melhor a importância desses métodos para os desenvolvedores. Muitos costumam não dar a devida importância para a sobrescrita correta destes métodos, o que pode ocasionar problemas bem críticos no código em determinadas situações (principalmente quando falamos de serialização de objetos e ambientes de alta concorrência). Você, inclusive, pode acompanhar um problema decorrente da sobrescrita e utilização incorretas destes métodos em um ambiente real neste post do StackOverflow.

Tem alguma dúvida? Quer discutir sobre algum determinado ponto? Quer expor uma situação pela qual você já passou no seu dia-a-dia como desenvolvedor que envolvia a utilização destes métodos? Compartilha com a gente nos comentários! Vamos discutir sobre este assunto! o/

Até o próximo post! =)

Deixe seu comentário

Líder de Conteúdo/Inovação. Pós-graduado em Projeto e Desenvolvimento de Aplicações Web. Certificado Microsoft MCSD (Web).

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES