Métodos Comuns a Todos Objetos

Este artigo trata sobre os métodos não finais da classe
Object. É responsabilidade do programador sobrescrevê-los em suas classes de forma adequada, garantindo o bom funcionamento de outras classes que os utilizam.

Item 7: Obedeça ao contrato geral quando sobrescrever o
equals
As conseqüências por sobrescrever o
equals de forma errada podem ser desastrosas. O jeito mais fácil de evitar problemas é simplesmente não sobrescrevê-lo, de forma que cada instância é igual apenas a ela mesma. Esta é a abordagem correta nas seguintes situações:
- Cada instância da classe é intrinsecamente única. Isto aplica-se a classes que representam entidades ao invés de valores, como por exemplo a
Thread.
- Não é importante que a classe provenha um teste lógico de igualdade, pois os clientes não precisarão desta funcionalidade.
- Uma superclasse já sobrescreveu o
equals e o comportamento herdado é adequado para a classe.
- A classe é privada ou com acesso somente dentro do pacote e você tem certeza de que o método
equals nunca será chamado. Nesse caso é mais apropriado que o método seja sobrescrito lançando uma UnsupportedOperationException, de forma a garantir que não seja utilizado futuramente.
De forma análoga, é adequado sobrescrever o
equals em classes de valores, como a Integer e a Date, em que existe a noção de igualdade lógica. Desta forma acontece o comportamento esperado quando utilizadas como chaves em Maps ou como elementos de Sets. Mas se uma superclasse já sobrescreveu o método adequadamente, ou na classe é garantida a existência de apenas um objeto para cada valor, não é necessária uma nova implementação.
Ao sobrescrever o método
equals, é necessário obedecer a seu contrato, implementando uma relação de equivalência com as seguintes características:
- Reflexiva:
x.equals(x) == true
- Simétrica:
x.equals(y) == true se e somente se y.equals(x) == true. Para evitar problemas neste caso, deve-se comparar somente objetos da mesma classe, pois não é possível garantir como está sendo realizada a implementação do equals em outra classe.
- Transitiva: Se
x.equals(y) == true e y.equals(z) == true então x.equals(z) == trueEste item é particularmente importante no caso de estender uma classe não abstrata, pois não é possível incluir um novo aspecto sem violar esta regra ou a de simetria no equals. Na API do Java, por exemplo, a classe Timestamp viola a regra de simetria ao herdar a Date, conforme explicado na sua documentação. Quando acontecer um caso semelhante, é melhor utilizar composição ao invés de herança.
- Consistente: chamadas múltiplas de
x.equals(y) sempre retornam true ou sempre retornam false se nenhuma informação usada no equals é modificada nos objetos. (esta regra é mais significativa nos casos de objetos imutáveis).
- Para qualquer referência não nula de x,
x.equals(null) == false. É importante que o código deste método verifique esta possibilidade e retorne false, ao invés de permitir que seja lançada uma NullPointerException durante sua execução. Utilizando o operador instanceof antes de fazer o cast do objeto esse problema já é resolvido.
Desta forma, os passos para sobrescrever o
equals adequadamente são:
1. Usar o operador == para checar se o argumento é uma referência ao mesmo objeto, o que poupa processamento caso a comparação dos objetos tenha problemas de performance.
2. Usar o
instanceof para checar se o argumento é do tipo correto. Ele pode ser comparado com a própria classe ou com uma interface implementada por esta classe (desde que sejam obedecidas as regras na implementação das outras classes).
3. Realizar o cast do argumento para o tipo correto.
4. Para cada campo significativo da classe, verificar o valor correspondente do campo do objeto passado como argumento. Para objetos, deve ser aplicado o
equals recursivamente, testando se ele é nulo antes de aplicar o método. No caso de tipos primitivos, exceto float e double, utiliza-se o operador ==. O float deve ser convertido para int através do método Float.floatToIntBits e o double para long através de Double.doubleToLongBits. Para arrays, deve ser comparado cada elemento.
A performance do método pode ser afetada pela ordem em que são comparados os campos. Para otimizar o processo, é melhor comparar primeiro os campos que têm maior probabilidade de estarem diferentes ou cuja comparação seja mais rápida. Normalmente não é necessário comparar campos redundantes, calculados a partir de outros campos, mas isso pode ser feito caso o campo sozinho já sumarize todo o objeto, poupando tempo caso esta comparação já retorne falso.
Observações finais:
- Sempre sobrescreva o
hashCode quando sobrescrever o equals.
- Utilize métodos simples de comparação, não tentando inventar mecanismos mais complexos que podem causar problemas.
- Não escreva um método
equals que dependa de recursos incertos, tais como uma conexão de rede. É melhor que dependa apenas de objetos residentes em memória, garantindo a consistência.
- Não substitua o
Object por outro tipo na assinatura do método, pois nesse caso você está fazendo overload do método e não sobrescrevendo-o.

Item 8: Sempre sobrescreva o hashCode quando sobrescrever o equals
Não sobrescrever o hashCode e alterar o equals viola o seu contrato, o que causará problemas ao usar a classe juntamente com collections que dependem deste método, tais como HashMap, HashSet e Hashtable. O método hashCode deve obedecer às seguintes regras:
- Sempre que for chamado no mesmo objeto mais de uma vez durante a execução de uma aplicação ele deve retornar o mesmo número inteiro, desde que nenhuma informação utilizada no
equals tenha sido modificada.
- Se dois objetos são iguais de acordo com o método
equals, então chamando o hashCode nos dois objetos deve retornar o mesmo resultado. Daí a importância de sobrescrever um método quando o outro for sobrescrito.
- Não é necessário que objetos diferentes produzam hash codes distintos, mas isso tem influência direta na performance de hash tables.
Portanto, a forma mais simples possível de obedecer ao contrato é simplesmente retornar um valor constante. Porém, esta técnica não deve ser utilizada, já que não estaria ajudando em nada. Idealmente, os objetos deveriam ser distribuídos uniformemente dentro de uma collection, o que é extremamente difícil de ser atingido. Mas uma aproximação pode ser obtida pelos seguintes passos:
1. Guarde um valor diferente de zero (ex. 17) em uma variável
int chamada result
2. Para cada campo considerado no equals, faça o seguinte:
   a. calcule um hashCode para o campo (f) e jogue em uma variável c:
      i. Se o campo for boleano, calcule (f ? 0 : 1)
      ii. Se o campo for byte, char, short ou int, calcule (int) f
      iii. Se o campo for long, calcule (int) (f ^ ( f >>> 32))
      iv. Se o campo for float, calcule Float.floatToIntBits
      v. Se o campo for double, calcule Double.doubleToLongBits e então faça o hash do resultado pelo passo 2.a.iii
      vi. Se o campo for uma referência a objeto e a classe, realize a comparação do campo chamando o equals recursivamente e chame o hashCode recursivamente. Se for necessária uma comparação mais complexa, utilize uma "representação canônica" do campo e chame o método de hashCode nela. Se o valor do campo for nulo, retorne 0
      vi. Se o campo for uma array, trate cada elemento como um campo separado e combine os valores como descrito no método 2.b
   b. Combine o hash code c, obtido no passo a, na variável result, da seguinte forma: result = 37 * result + c;
3. Retorne a variável
result
4. Revise o método hashCode e equals verificando se instâncias iguais estão retornando o mesmo hash code, de forma a obedecer ao contrato. É imprescindível que qualquer campo não derivado que não é utilizado no equals não seja utilizado também no hashCode, para não violar a segunda regra do contrato.
A multiplicação no passo 2.b faz com que a ordem dos campos seja considerada, e o 37 foi escolhido por ser um número
ímpar primo. Números pares não devem ser utilizados porque caso a multiplicação fique muito grande seria perdida informação, pois multiplicação por 2 é equivalente a uma operação de shift.
Para melhorar a performance, se a classe for imutável pode ser considerada a possibilidade de guardar o valor do hash code no objeto ao invés de recalculá-lo toda vez. Uma má idéia é excluir campos significativos do cálculo, pois isso poderá impactar a performance no uso de hash tables posteriormente.

Item 9: Sempre sobrescreva o toString
O método original toString, herdado da classe Object, consiste no nome da classe seguido por um sinal de arroba e a representação hexadecimal do hash code. O contrato para esse método diz que a string retornada deve ser uma representação informativa e concisa e deve ser fácil para uma pessoa ler. Uma boa implementação deste método torna a classe mais agradável de ser usada, pois ele é chamado automaticamente quando o objeto é passado em um println, ao usar o operador de concatenação (+) ou juntamente com o assert.
Quando viável, o método
toString deve retornar todas as informações relevantes contidas no objeto. Mas pode ser criado um tipo de resumo que identifique o objeto quando ele for muito grande ou tiver alguma informação que não seja possível representar por uma string. Idealmente, o valor retornado é auto-explicativo.
No caso de classes de valores, uma boa opção é documentar o formato do retorno deste método e prover um construtor que receba uma string neste formato para criar o objeto, tornando possível que outros programadores traduzam facilmente de objeto para string e vice-versa. Porém, ao fazer isso torna-se perigoso mudar este formato, pois outras classes provavelmente estarão dependendo dele. Qualquer que seja a decisão, é importante deixar claro na documentação o formato ou então a possibilidade de mudança desta representação.
Independentemente de especificar ou não o formato, é sempre uma boa idéia ter métodos que acessem diretamente toda a informação retornada pelo método
toString, evitando que os outros programadores sejam forçados a obtê-la através de parse da string, o que reduz a performance e está mais sujeito a erros.

Item 10: Sobrescreva o clone cuidadosamente
A interface Clonable foi criada para indicar que o objeto permite clonagem. Porém, ela falha nesse aspecto, pois não inclui o método clone e o método da classe Object tem acesso protected , não sendo possível chamar o método pelo simples fato de ter a interface implementada. O que a Cloneable faz na verdade é determinar o comportamento da implementação original do método clone: se ela tiver sido implementada, será feita uma cópia campo a campo do objeto, caso contrário, será lançada uma CloneNotSupportedException.
O contrato para a implementação deste método é bem vago, dando apenas algumas sugestões. De forma geral, a cópia de um objeto envolve a criação de uma nova instância da classe (sem utilizar o construtor) que pode ser seguida de uma cópia dos dados internos.
Se você sobrescrever o método
clone de uma classe que não seja final, você deve retornar o objeto criado através de super.clone, pois desta forma as subclasses podem fazer o mesmo e obter um objeto de sua própria classe, que será retornado pelo método da classe Object em um mecanismo similar ao de encadeamento de construtores. Utilizando apenas o super.clone, você já obtém um objeto da mesma classe e com todos os campos com valores iguais aos do original. Mas observe que se algum desses campos fizer referência a um objeto mutável, o clone e a instância original estarão apontando para o mesmo objeto, que pode ser alterado por qualquer uma delas, ficando sujeito a resultados inesperados. Para evitar isso, deve ser chamado também o método clone destes objetos. Porém, se existirem campos finais referindo-se a objetos mutáveis, não será possível copiá-los dessa forma, sendo necessário remover o atributo final ou aceitar as conseqüências de compartilhar o objeto. Em alguns casos, quando as classes dos objetos utilizados internamente estão fora do padrão, não adianta apenas chamar o método clone recursivamente, sendo necessário criar um mecanismo mais elaborado para copiar os objetos internos. Também é necessário tratar os campos internamente quando eles representam atributos únicos, como um ID ou data de criação. A última abordagem que pode ser utilizada é criar um novo objeto através da chamada ao super.clone, reinicializar todos os campos e chamar outros métodos da classe específicos para gerar novamente o estado do objeto.
Outro ponto a ser considerado é que o método
clone, da mesma forma que construtores, não deve chamar métodos não finais, pois caso o método tenha sido sobrescrito ele pode ser chamado antes da subclasse ter tipo a oportunidade de arrumar seu estado interno no objeto clonado, podendo gerar um estado corrompido.
Em relação a
CloneNotSupportedException, classes finais podem omití-la da declaração do método, tornando mais prático o seu uso. Porém, se a classe for desenhada para ser herdada, é melhor manter a exceção na declaração, pois assim as subclasses podem optar por não suportar a clonagem de forma mais elegante.
De forma geral, você só é obrigado a prover um método
clone próprio se está herdando de uma classe que implemente a interface Cloneable. Para outros casos, costuma ser mais prático simplesmente criar outros mecanismos para copiar um objeto, como por exemplo um construtor ou método static factory com esta finalidade, que apresentam diversas vantagens em relação ao método clone.

Item 11: Considere a implementação de Comparable
Ao contrário dos outros métodos discutidos neste artigo, o método compareTo não é declarado na classe Object, e sim na interface java.lang.Comparable. Implementando esta interface a classe indica que possiu uma ordem natural, sendo possível ordenar e comparar objetos.
O contrato do método
compareTo manda que ele retorne um inteiro negativo, zero ou um inteiro positivo, se o objeto for menor, igual ou maior que o objeto especificado, respectivamente. Ele define também que será lançada uma ClassCastException se  o tipo do objeto for incompatível com o que está sendo comparado. Há ainda algumas regras relacionadas à função matemática signum, que retorna -1, 0 ou 1 de acordo com o sinal da expressão dada:
-
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) - isto implica ainda que só poderá ser lançada uma exceção em um dos lados da igualdade se também for lançada no outro
- x.compareTo(y) > 0 && y.compareTo(z) > 0 implica em x.compareTo(z) > 0
-
x.compareTo(y) > 0 implica em sgn(x.compareTo(z)) == sgn(y.compareTo(z) para qualquer z
- É recomendável, mas não necessário, que
(x.compareTo(y)==0) == (x.equals(y)). Quando isso não for verdadeiro deve estar indicado na documentação.
As três primeiras regras implicam nas mesmas propriedades discutidas no método
equals: reflexividade, simetria, transitividade e não-nulidade. Conseqüentemente, as mesmas observações e limitações discutidas no item 7 são aplicáveis aqui. A implementação também segue uma metodologia semelhante, mas no caso do compareTo é esperado que sejam lançadas exceções caso ocorram problemas de cast ou argumentos nulos, não sendo necessárias as verificações destes casos. A comparação de campos referentes a objetos é feita chamando o método compareTo recursivamente e de campos primitivos utilizando os operadores < e >. Se a classe tem vários campos significativos, é necessário começar pelo mais significativo e ir prosseguindo até que a comparação resulte em algo diferente de zero, retornando este resultado. Ao invés de comparar tipos primitivos com os operadores < e > e então retornar -1 ou +1, você pode calcular a diferença entre os valores e retorná-la diretamente. Porém, esta técnica deve ser utilizada apenas em casos específicos, observando que o número não seja negativo e que a diferença entre eles seja menor que (2³¹ - 1), pois estes casos causariam inconsistência no resultado.
Uma classe que viole o contrato poderá causar erros ao ser utilizada em outras classes que dependam de comparação, tais como TreeSet, TreeMap, Collections e Arrays.



Bibliografia:
Bloch, Joshua. Effective Java.

Resumo por: Vanessa Sabino
1