equals
estão hashCode
intimamente relacionados entre si e que é aconselhável substituir ambos os métodos em suas classes de forma consistente. Um número um pouco menor sabe por que isso acontece e quais consequências tristes podem ocorrer se essa regra for quebrada. Proponho considerar o conceito desses métodos, repetir seu propósito e entender por que estão tão interligados. Escrevi este artigo, assim como o anterior sobre carregamento de classes, para finalmente revelar todos os detalhes do problema e não retornar mais a fontes de terceiros. Portanto, terei prazer em receber críticas construtivas, porque se houver lacunas em algum lugar, elas deverão ser eliminadas. O artigo, infelizmente, acabou sendo bastante extenso.
regras de substituição iguais
É necessário um métodoequals()
em Java para confirmar ou negar o fato de que dois objetos da mesma origem são logicamente iguais . Ou seja, ao comparar dois objetos, o programador precisa entender se seus campos significativos são equivalentes . Não é necessário que todos os campos sejam idênticos, pois o método equals()
implica igualdade lógica . Mas às vezes não há necessidade específica de usar esse método. Como se costuma dizer, a maneira mais fácil de evitar problemas ao usar um determinado mecanismo é não usá-lo. Deve-se notar também que, ao quebrar um contrato, equals
você perde o controle da compreensão de como outros objetos e estruturas irão interagir com o seu objeto. E, posteriormente, será muito difícil encontrar a causa do erro.
Quando não substituir este método
- Quando cada instância de uma classe é única. Em maior medida, isso se aplica às classes que fornecem comportamento específico, em vez de serem projetadas para trabalhar com dados. Tal, por exemplo, como a classe
- Quando na verdade a classe não é obrigada a determinar a equivalência de suas instâncias. Por exemplo, para uma classe
- Quando a classe que você está estendendo já possui sua própria implementação do método
equals
e o comportamento dessa implementação combina com você. Por exemplo, para classes ,, - E por fim, não há necessidade de override
equals
quando o escopo da sua classe forprivate
oupackage-private
e você tiver certeza que esse método nunca será chamado.
Thread
. Para eles equals
, a implementação do método fornecido pela classe Object
é mais que suficiente. Outro exemplo são as classes enum ( Enum
).
java.util.Random
não há necessidade de comparar instâncias da classe entre si, determinando se elas podem retornar a mesma sequência de números aleatórios. Simplesmente porque a natureza desta classe nem sequer implica tal comportamento.
Set
a implementação está em e , respectivamente. List
Map
equals
AbstractSet
AbstractList
AbstractMap
contrato igual
Ao substituir um método,equals
o desenvolvedor deve aderir às regras básicas definidas na especificação da linguagem Java.
- Reflexividade para qualquer valor fornecido
- Simetria para quaisquer valores fornecidos
- Transitividade para quaisquer valores fornecidos e
- Consistência para quaisquer valores fornecidos,
- Comparação nula para qualquer valor fornecido,
x
, a expressão x.equals(x)
deve retornar true
.
Dado - significando tal que
x != null
x
e y
, x.equals(y)
deve retornar true
apenas se y.equals(x)
retornar true
.
x
, se retornar e retornar , deverá retornar o valor . y
z
x.equals(y)
true
y.equals(z)
true
x.equals(z)
true
x
e y
a chamada repetida x.equals(y)
retornará o valor da chamada anterior para este método, desde que os campos usados para comparar os dois objetos não tenham mudado entre as chamadas.
x
a chamada x.equals(null)
deve retornar false
.
é igual a violação do contrato
Muitas classes, como as do Java Collections Framework, dependem da implementação do métodoequals()
, portanto você não deve negligenciá-lo, pois A violação do contrato deste método pode levar ao funcionamento irracional da aplicação e, neste caso, será bastante difícil encontrar o motivo. De acordo com o princípio da reflexividade , todo objeto deve ser equivalente a si mesmo. Se este princípio for violado, quando adicionarmos um objeto à coleção e depois procurá-lo usando o método, contains()
não conseguiremos encontrar o objeto que acabamos de adicionar à coleção. A condição de simetria afirma que quaisquer dois objetos devem ser iguais, independentemente da ordem em que são comparados. Por exemplo, se você tiver uma classe contendo apenas um campo do tipo string, será incorreto comparar equals
esse campo com uma string em um método. Porque no caso de comparação reversa, o método sempre retornará o valor false
.
// Нарушение симметричности
public class SomeStringify {
private String s;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof SomeStringify) {
return s.equals(((SomeStringify) o).s);
}
// нарушение симметричности, классы разного происхождения
if (o instanceof String) {
return s.equals(o);
}
return false;
}
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof SomeStringify &&
((SomeStringify) o).s.equals(s);
}
Da condição de transitividade segue-se que se dois dos três objetos forem iguais, então, neste caso, todos os três devem ser iguais. Este princípio pode ser facilmente violado quando é necessário estender uma determinada classe base adicionando-lhe um componente significativo . Por exemplo, para uma classe Point
com coordenadas x
e y
você precisa adicionar a cor do ponto expandindo-o. Para fazer isso, você precisará declarar uma classe ColorPoint
com um campo correspondente color
. Assim, se na classe estendida chamamos o equals
método pai, e no pai assumimos que apenas as coordenadas x
e são comparadas y
, então dois pontos de cores diferentes, mas com as mesmas coordenadas, serão considerados iguais, o que é incorreto. Nesse caso, é necessário ensinar a classe derivada a distinguir cores. Para fazer isso, você pode usar dois métodos. Mas um violará a regra da simetria e o segundo da transitividade .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
Nesse caso, a chamada point.equals(colorPoint)
retornará o valor true
e a comparação colorPoint.equals(point)
retornará false
, porque espera um objeto de “sua” classe. Assim, a regra da simetria é violada. O segundo método envolve fazer uma verificação “cega” no caso em que não há dados sobre a cor do ponto, ou seja, temos a classe Point
. Ou verifique a cor se houver informações disponíveis sobre ela, ou seja, compare um objeto da classe ColorPoint
.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
// Слепая проверка
if (!(o instanceof ColorPoint))
return super.equals(o);
// Полная проверка, включая цвет точки
return super.equals(o) && ((ColorPoint) o).color == color;
}
O princípio da transitividade é violado aqui da seguinte forma. Digamos que haja uma definição dos seguintes objetos:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Assim, embora a igualdade p1.equals(p2)
e seja satisfeita p2.equals(p3)
, p1.equals(p3)
ela retornará o valor false
. Ao mesmo tempo, o segundo método, na minha opinião, parece menos atraente, porque Em alguns casos, o algoritmo pode ficar cego e não realizar a comparação totalmente, e você pode não saber disso. Um pouco de poesia Em geral, pelo que entendi, não existe uma solução concreta para este problema. Há uma opinião de um autor autorizado chamado Kay Horstmann de que você pode substituir o uso do operador instanceof
por uma chamada de método getClass()
que retorna a classe do objeto e, antes de começar a comparar os próprios objetos, certifique-se de que eles são do mesmo tipo , e não prestam atenção ao fato de sua origem comum. Assim, as regras de simetria e transitividade serão satisfeitas. Mas, ao mesmo tempo, do outro lado da barricada está outro autor, não menos respeitado em amplos círculos, Joshua Bloch, que acredita que esta abordagem viola o princípio da substituição de Barbara Liskov. Este princípio afirma que “o código de chamada deve tratar uma classe base da mesma maneira que suas subclasses, sem saber disso ” . E na solução proposta por Horstmann esse princípio é claramente violado, pois depende da implementação. Em suma, é claro que o assunto é obscuro. Deve-se notar também que Horstmann esclarece a regra para aplicar sua abordagem e escreve em inglês simples que você precisa decidir sobre uma estratégia ao projetar classes, e se o teste de igualdade for realizado apenas pela superclasse, você pode fazer isso executando a operação instanceof
. Caso contrário, quando a semântica da verificação mudar dependendo da classe derivada e a implementação do método precisar ser movida para baixo na hierarquia, você deverá usar o método getClass()
. Joshua Bloch, por sua vez, propõe abandonar a herança e usar a composição de objetos, incluindo uma ColorPoint
classe na classe Point
e fornecendo um método de acesso asPoint()
para obter informações específicas sobre o ponto. Isso evitará quebrar todas as regras, mas, na minha opinião, tornará o código mais difícil de entender. A terceira opção é usar a geração automática do método equals usando o IDE. A ideia, aliás, reproduz a geração de Horstmann, permitindo escolher uma estratégia para implementar um método em uma superclasse ou em seus descendentes. Finalmente, a próxima regra de consistência afirma que mesmo que os objetos x
não y
mudem, chamá-los novamente x.equals(y)
deve retornar o mesmo valor de antes. A regra final é que nenhum objeto deve ser igual a null
. Tudo está claro aqui null
- isso é incerteza, o objeto é igual à incerteza? Não está claro, ou seja false
.
Algoritmo geral para determinar iguais
- Verifique a igualdade de referências de objetos
this
e parâmetros de métodoso
.if (this == o) return true;
- Verifique se o link está definido
o
, ou seja, se estánull
.
Se futuramente, ao comparar tipos de objetos, for utilizado o operadorinstanceof
, este item poderá ser ignorado, pois este parâmetro retornafalse
neste casonull instanceof Object
. - Compare os tipos de objetos
this
usandoo
um operadorinstanceof
ou métodogetClass()
, guiado pela descrição acima e pela sua própria intuição. - Se um método
equals
for substituído em uma subclasse, certifique-se de fazer uma chamadasuper.equals(o)
- Converta o tipo de parâmetro
o
na classe necessária. - Execute uma comparação de todos os campos de objetos significativos:
- para tipos primitivos (exceto
float
edouble
), usando o operador==
- para campos de referência você precisa chamar seu método
equals
- para matrizes, você pode usar a iteração cíclica ou o método
Arrays.equals()
- para tipos
float
edouble
é necessário usar métodos de comparação das classes wrapper correspondentesFloat.compare()
eDouble.compare()
- para tipos primitivos (exceto
- E por fim, responda três perguntas: o método implementado é simétrico ? Transitivo ? Acordado ? Os outros dois princípios ( reflexividade e certeza ) são geralmente implementados automaticamente.
Regras de substituição de HashCode
Um hash é um número gerado a partir de um objeto que descreve seu estado em algum momento. Este número é usado em Java principalmente em tabelas hash comoHashMap
. Neste caso, a função hash de obtenção de um número com base em um objeto deve ser implementada de forma a garantir uma distribuição relativamente uniforme dos elementos na tabela hash. E também para minimizar a probabilidade de colisões quando a função retorna o mesmo valor para chaves diferentes.
HashCode do contrato
Para implementar uma função hash, a especificação da linguagem define as seguintes regras:- chamar um método
hashCode
uma ou mais vezes no mesmo objeto deve retornar o mesmo valor de hash, desde que os campos do objeto envolvidos no cálculo do valor não tenham mudado. - chamar um método
hashCode
em dois objetos deve sempre retornar o mesmo número se os objetos forem iguais (chamar um métodoequals
nesses objetos retornatrue
). - chamar um método
hashCode
em dois objetos desiguais deve retornar valores de hash diferentes. Embora este requisito não seja obrigatório, deve-se considerar que a sua implementação terá um efeito positivo no desempenho das tabelas hash.
Os métodos equals e hashCode devem ser substituídos juntos
Com base nos contratos descritos acima, segue-se que ao substituir o método no seu códigoequals
, você deve sempre substituir o método hashCode
. Como na verdade duas instâncias de uma classe são diferentes porque estão em áreas de memória diferentes, elas devem ser comparadas de acordo com alguns critérios lógicos. Conseqüentemente, dois objetos logicamente equivalentes devem retornar o mesmo valor de hash. O que acontece se apenas um desses métodos for substituído?
-
equals
simhashCode
nãoDigamos que definimos corretamente um método
equals
em nossa classe ehashCode
decidimos deixar o método como está na classeObject
. Então, do ponto de vista do método,equals
os dois objetos serão logicamente iguais, enquanto do ponto de vista do métodohashCode
eles não terão nada em comum. E assim, ao colocar um objeto em uma tabela hash, corremos o risco de não recuperá-lo por chave.
Por exemplo, assim:Map<Point, String> m = new HashMap<>(); m.put(new Point(1, 1), “Point A”); // pointName == null String pointName = m.get(new Point(1, 1));
Obviamente, o objeto que está sendo colocado e o objeto que está sendo procurado são dois objetos diferentes, embora sejam logicamente iguais. Mas porque eles têm valores de hash diferentes porque violamos o contrato, podemos dizer que perdemos nosso objeto em algum lugar nas entranhas da tabela de hash.
-
hashCode
simequals
não.O que acontece se substituirmos o método
hashCode
eequals
herdarmos a implementação do método da classeObject
. Como você sabe, oequals
método padrão simplesmente compara ponteiros a objetos, determinando se eles se referem ao mesmo objeto. Suponhamos quehashCode
escrevemos o método de acordo com todos os cânones, ou seja, o geramos usando o IDE, e ele retornará os mesmos valores de hash para objetos logicamente idênticos. Obviamente, ao fazer isso já definimos algum mecanismo para comparar dois objetos.Portanto, o exemplo do parágrafo anterior deveria, em teoria, ser executado. Mas ainda não conseguiremos encontrar nosso objeto na tabela hash. Embora estaremos perto disso, porque no mínimo encontraremos uma cesta de mesa hash na qual o objeto ficará.
Para pesquisar com sucesso um objeto em uma tabela hash, além de comparar os valores hash da chave, também é utilizada a determinação da igualdade lógica da chave com o objeto pesquisado. Ou seja,
equals
não há como fazer sem substituir o método.
Algoritmo geral para determinar hashCode
Aqui, me parece, você não deve se preocupar muito e gerar o método no seu IDE favorito. Porque todas essas mudanças de bits para a direita e para a esquerda em busca da proporção áurea, ou seja, distribuição normal - isso é para caras completamente teimosos. Pessoalmente, duvido que consiga fazer melhor e mais rápido que a mesma ideia.Em vez de uma conclusão
Assim, vemos que os métodosequals
desempenham hashCode
um papel bem definido na linguagem Java e são projetados para obter a característica de igualdade lógica de dois objetos. No caso do método, equals
isso tem relação direta com a comparação de objetos, no caso hashCode
indireto, quando é necessário, digamos, determinar a localização aproximada de um objeto em tabelas hash ou estruturas de dados semelhantes, a fim de aumentar a velocidade de busca por um objeto. Além dos contratos , equals
existe hashCode
outra exigência relacionada à comparação de objetos. Esta é a consistência de um método compareTo
de interface Comparable
com um arquivo equals
. Este requisito obriga o desenvolvedor a sempre retornar x.equals(y) == true
quando x.compareTo(y) == 0
. Ou seja, vemos que a comparação lógica de dois objetos não deve contradizer em nenhum lugar da aplicação e deve ser sempre consistente.
GO TO FULL VERSION