JavaRush /Blogue Java /Random-PT /Contratos iguais e hashCode ou o que quer que seja
Aleksandr Zimin
Nível 1
Санкт-Петербург

Contratos iguais e hashCode ou o que quer que seja

Publicado no grupo Random-PT
A grande maioria dos programadores Java, é claro, sabe que os métodos equalsestão hashCodeintimamente 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étodo equals()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, equalsvocê 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 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).
  • Quando na verdade a classe não é obrigada a determinar a equivalência de suas instâncias.
  • Por exemplo, para uma classe java.util.Randomnã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.
  • Quando a classe que você está estendendo já possui sua própria implementação do método equalse o comportamento dessa implementação combina com você.
  • Por exemplo, para classes ,, Seta implementação está em e , respectivamente. ListMapequalsAbstractSetAbstractListAbstractMap
  • E por fim, não há necessidade de override equalsquando o escopo da sua classe for privateou package-privatee você tiver certeza que esse método nunca será chamado.

contrato igual

Ao substituir um método, equalso desenvolvedor deve aderir às regras básicas definidas na especificação da linguagem Java.
  • Reflexividade
  • para qualquer valor fornecido x, a expressão x.equals(x)deve retornar true.
    Dado - significando tal quex != null
  • Simetria
  • para quaisquer valores fornecidos xe y, x.equals(y)deve retornar trueapenas se y.equals(x)retornar true.
  • Transitividade
  • para quaisquer valores fornecidos e x, se retornar e retornar , deverá retornar o valor . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Consistência
  • para quaisquer valores fornecidos, xe ya 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.
  • Comparação nula
  • para qualquer valor fornecido, xa 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étodo equals(), 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 equalsesse 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 Pointcom coordenadas xe yvocê precisa adicionar a cor do ponto expandindo-o. Para fazer isso, você precisará declarar uma classe ColorPointcom um campo correspondente color. Assim, se na classe estendida chamamos o equalsmétodo pai, e no pai assumimos que apenas as coordenadas xe 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 truee 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 instanceofpor 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 ColorPointclasse na classe Pointe 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 xnão ymudem, 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

  1. Verifique a igualdade de referências de objetos thise parâmetros de métodos o.
    if (this == o) return true;
  2. Verifique se o link está definido o, ou seja, se está null.
    Se futuramente, ao comparar tipos de objetos, for utilizado o operador instanceof, este item poderá ser ignorado, pois este parâmetro retorna falseneste caso null instanceof Object.
  3. Compare os tipos de objetos thisusando oum operador instanceofou método getClass(), guiado pela descrição acima e pela sua própria intuição.
  4. Se um método equalsfor substituído em uma subclasse, certifique-se de fazer uma chamadasuper.equals(o)
  5. Converta o tipo de parâmetro ona classe necessária.
  6. Execute uma comparação de todos os campos de objetos significativos:
    • para tipos primitivos (exceto floate double), usando o operador==
    • para campos de referência você precisa chamar seu métodoequals
    • para matrizes, você pode usar a iteração cíclica ou o métodoArrays.equals()
    • para tipos floate doubleé necessário usar métodos de comparação das classes wrapper correspondentes Float.compare()eDouble.compare()
  7. 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 como HashMap. 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 hashCodeuma 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 hashCodeem dois objetos deve sempre retornar o mesmo número se os objetos forem iguais (chamar um método equalsnesses objetos retorna true).
  • chamar um método hashCodeem 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ódigo equals, 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?
  1. equalssim hashCodenão

    Digamos que definimos corretamente um método equalsem nossa classe e hashCodedecidimos deixar o método como está na classe Object. Então, do ponto de vista do método, equalsos dois objetos serão logicamente iguais, enquanto do ponto de vista do método hashCodeeles 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.

  2. hashCodesim equalsnão.

    O que acontece se substituirmos o método hashCodee equalsherdarmos a implementação do método da classe Object. Como você sabe, o equalsmétodo padrão simplesmente compara ponteiros a objetos, determinando se eles se referem ao mesmo objeto. Suponhamos que hashCodeescrevemos 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, equalsnã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étodos equalsdesempenham hashCodeum 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, equalsisso tem relação direta com a comparação de objetos, no caso hashCodeindireto, 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 , equalsexiste hashCodeoutra exigência relacionada à comparação de objetos. Esta é a consistência de um método compareTode interface Comparablecom um arquivo equals. Este requisito obriga o desenvolvedor a sempre retornar x.equals(y) == truequando 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.

Fontes

Java Efetivo, Segunda Edição. Josué Bloch. Tradução gratuita de um livro muito bom. Java, uma biblioteca para profissionais. Volume 1. Noções básicas. Kay Horstmann. Um pouco menos de teoria e mais prática. Mas nem tudo é analisado com tantos detalhes como o de Bloch. Embora haja uma visão sobre o mesmo equals(). Estruturas de dados em imagens. HashMap Um artigo extremamente útil sobre o dispositivo HashMap em Java. Em vez de olhar para as fontes.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION