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) == true. Este 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