Uma instância poderia ser igual a outra instância de um tipo mais específico?

25

Eu li este artigo: Como escrever um método de igualdade em Java .

Basicamente, ele fornece uma solução para um método equals () que suporta herança:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

Mas é uma boa idéia? essas duas instâncias parecem iguais, mas podem ter dois códigos de hash diferentes. Isso não está um pouco errado?

Acredito que isso seria melhor alcançado ao lançar os operandos.

Wes
fonte
1
O exemplo com pontos coloridos, conforme indicado no link, faz mais sentido para mim. Eu consideraria que um ponto 2D (x, y) pode ser visto como um ponto 3D com um componente Z zero (x, y, 0) e gostaria que a igualdade retornasse false no seu caso. De fato, no artigo, diz-se explicitamente que um ColoredPoint é diferente de um Point e sempre retorna false.
Coredump #
10
Nada pior do que tutoriais que quebram convenções comuns ... Leva anos para quebrar esses tipos de hábitos dos programadores.
corsiKa
3
@coredump Tratar um ponto 2D como tendo uma zcoordenada zero pode ser uma convenção útil para algumas aplicações (os primeiros sistemas CAD que lidam com dados legados vêm à mente). Mas é uma convenção arbitrária. Aviões em espaços com 3 ou mais dimensões podem ter orientações arbitrárias ... é o que torna interessantes problemas interessantes.
ben rudgers
2
É mais do que um pouco errado .
21815 Kevin Krumwiede

Respostas:

71

Isso não deve ser igualdade porque quebra a transitividade . Considere estas duas expressões:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Como a igualdade é transitiva, isso significa que a seguinte expressão também é verdadeira:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

Mas é claro - não é.

Portanto, sua idéia de transmissão está correta - espere que, em Java, transmissão simplesmente signifique transmissão do tipo de referência. O que você realmente deseja aqui é um método de conversão que crie um novo Point2Dobjeto a partir de um Point3Dobjeto. Isso também tornaria a expressão mais significativa:

twoD.equals(threeD.projectXY())
Idan Arye
fonte
1
O artigo descreve implementações que interrompem a transitividade e oferece uma variedade de soluções alternativas. Em um domínio em que permitimos pontos 2D, já decidimos que a terceira dimensão não importa. e assim (10, 20, 50)igual (10, 20, 60)é bom. Nós nos preocupamos apenas com 10e 20.
ben rudgers
1
Deve Point2Dter um projectXYZ()método para fornecer uma Point3Drepresentação de si mesmo? Em outras palavras, as implementações devem se conhecer?
Hjk
4
@hjk Livrar-se de Point2Dparece mais simples, já que projetar pontos 2D requer a definição de seu plano no espaço 3D primeiro. Se o ponto 2D sabe que é plano, já é um ponto 3D. Se não, não pode projetar. Lembro-me do Flatland de Abbott .
ben rudgers
@benrudgers Você pode, no entanto, definir um Plane3Dobjeto, que definirá um plano no espaço 3D, esse plano pode ter um liftmétodo (2D-> 3D está levantando, não projetando) que aceitará um Point2De um número para o "terceiro eixo" "- distância do plano ao longo do plano normal. Para facilidade de uso, você pode definir os aviões comuns como constantes estáticas, assim que você poderia fazer coisas comoPlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye
@IdanArye Eu estava comentando sobre a sugestão de que os pontos 2D deveriam ter um método de projeção. Quanto aos planos com métodos de elevação, acho que seriam necessários dois argumentos para fazer sentido: um ponto 2D e o plano em que se supõe estar, ou seja, ele realmente precisa ser uma projeção se não for o proprietário do ponto ... e se ele é o dono do argumento, por que não apenas possuir um ponto 3D e acabar com um tipo de dados problemático e com o cheiro de um método criticado? YMMV.
ben rudgers
10

Afasto-me da leitura do artigo pensando na sabedoria de Alan J. Perlis:

Epigrama 9. É melhor ter 100 funções operando em uma estrutura de dados do que 10 funções em 10 estruturas de dados.

O fato de acertar a "igualdade" é o tipo de problema que mantém o inventor de Scala, de Martin Ordersky, acordado à noite deve dar uma pausa sobre se a substituição equalsem uma árvore de herança é uma boa idéia.

O que acontece quando não temos sorte em obter uma ColoredPointé que nossa geometria falha porque usamos a herança para proliferar tipos de dados em vez de criar uma boa. Apesar de ter que voltar e modificar o nó raiz da árvore de herança para fazer o equalstrabalho. Por que não apenas adicionar um ze um colora Point?

O bom motivo seria isso Pointe ColoredPointoperaria em domínios diferentes ... pelo menos se esses domínios nunca se misturassem. No entanto, se for esse o caso, não precisamos substituir equals. A comparação ColoredPointe Pointa igualdade só fazem sentido em um terceiro domínio onde eles podem se misturar. E, nesse caso, provavelmente é melhor ter a "igualdade" adaptada ao terceiro domínio, em vez de tentar aplicar a semântica da igualdade de um ou outro ou de ambos os domínios não combinados. Em outras palavras, "igualdade" deve ser definida como local para o lugar em que temos lama fluindo de ambos os lados, porque talvez não desejemos ColoredPoint.equals(pt)falhar contra instâncias, Pointmesmo que o autor do livro tenha ColoredPointpensado que era uma boa idéia seis meses atrás às 2 da manhã. .

ben rudgers
fonte
6

Quando os antigos deuses da programação estavam inventando a programação orientada a objetos com classes, eles decidiram, quando se tratava de composição e herança, ter dois relacionamentos para um objeto: "é um" e "tem um".
Isso resolveu parcialmente o problema das subclasses serem diferentes das classes pai, mas as tornou utilizáveis ​​sem quebrar o código. Como uma instância de subclasse "é um" objeto de superclasse e pode ser substituída diretamente por ele, mesmo que a subclasse tenha mais funções-membro ou membros de dados, a "possui-a" garante que ele executará todas as funções do pai e terá todas as suas membros. Então, você poderia dizer que um Point3D "é um" Point e um Point2D "é um" Point se ambos herdarem do Point. Além disso, um Point3D pode ser uma subclasse de Point2D.

Entretanto, a igualdade entre as classes é específica do domínio do problema, e o exemplo acima é ambíguo quanto ao que o programador precisa para o programa funcionar corretamente. Geralmente, as regras do domínio matemático são seguidas e os valores dos dados geram igualdade se você limitar o escopo da comparação apenas neste caso a duas dimensões, mas não se você comparar todos os membros dos dados.

Então, você obtém uma tabela de igualdade de igualdade:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

Geralmente, você escolhe as regras mais rígidas que ainda pode executar todas as funções necessárias no domínio do problema. Os testes de igualdade internos para números são projetados para serem o mais restritivos possível para fins de matemática, mas o programador tem muitas maneiras de contornar isso, se esse não for o objetivo, incluindo arredondamento para cima / baixo, truncamento, gt, lt, etc. . Objetos com registro de data e hora são frequentemente comparados pelo tempo de geração e, portanto, cada instância deve ser única para que as comparações sejam muito específicas.

O fator de design nesse caso é determinar maneiras eficientes de comparar objetos. Às vezes, é necessário fazer uma comparação recursiva de todos os membros dos dados dos objetos, e isso pode ficar muito caro se você tiver muitos e muitos objetos com muitos membros dos dados. As alternativas são comparar apenas valores de dados relevantes ou fazer com que o objeto gere um valor de hash de seus membros de dados envolvidos para uma comparação rápida com outros objetos semelhantes, mantenha as coleções classificadas e removidas para tornar as comparações mais rápidas e menos intensivas na CPU, e talvez permitir objetos que são idênticos nos dados a serem descartados e um ponteiro duplicado para um único objeto é colocado em seu lugar.

Chris Reid
fonte
2

A regra é que, sempre que você substitui hashcode(), você substitui equals()e vice-versa. Se é uma boa ideia ou não, depende do uso pretendido. Pessoalmente, eu usaria um método diferente ( isLike()ou similar) para obter o mesmo efeito.

TMN
fonte
1
Pode ser bom substituir o hashCode sem substituir iguais. Por exemplo, alguém faria isso para testar um algoritmo de hash diferente para a mesma condição de igualdade.
Patricia Shanahan
1

Muitas vezes, é útil que classes não públicas tenham um método de teste de equivalência que permita que objetos de tipos diferentes se considerem "iguais" se representarem a mesma informação, mas porque o Java não permite meios pelos quais as classes podem se passar por elas. outro, geralmente é bom ter um único tipo de invólucro voltado para o público nos casos em que seja possível ter objetos equivalentes com representações diferentes.

Por exemplo, considere uma classe que encapsule uma matriz 2D imutável de doublevalores. Se um método externo solicita uma matriz de identidade do tamanho 1000, um segundo solicita uma matriz diagonal e passa uma matriz contendo 1000 unidades, e um terceiro solicita uma matriz 2D e passa uma matriz 1000x1000 onde os elementos na diagonal primária são todos 1,0 e todos os outros são zero, os objetos fornecidos para as três classes podem usar diferentes repositórios internamente [o primeiro com um único campo de tamanho, o segundo com uma matriz de mil elementos e o terceiro com mil matrizes de 1000 elementos], mas devem se reportar como equivalentes [já que todos os três encapsulam uma matriz imutável de 1000x1000 com as na diagonal e zeros em qualquer outro lugar].

Além do fato de ocultar a existência de tipos distintos de loja de apoio, o wrapper também será útil para facilitar comparações, pois a verificação de itens quanto à equivalência geralmente será um processo de várias etapas. Pergunte ao primeiro item se ele sabe se é igual ao segundo; se não souber, pergunte ao segundo se ele sabe se é igual ao primeiro. Se nenhum dos objetos souber, pergunte a cada matriz sobre o conteúdo de seus elementos individuais [pode-se adicionar outras verificações antes de decidir fazer a rota de comparação de itens individuais por muito tempo lenta].

Observe que o método de teste de equivalência para cada objeto nesse cenário precisaria retornar um valor de três estados ("Sim, eu sou equivalente", "Não, eu não sou equivalente" ou "Não sei"), portanto, o método "igual" normal não seria adequado. Embora qualquer objeto possa simplesmente responder "eu não sei" quando perguntado sobre qualquer outro, adicionar lógica a uma matriz diagonal, por exemplo, que não incomodaria perguntar a qualquer matriz de identidade ou matriz diagonal sobre quaisquer elementos fora da diagonal principal, agilizaria bastante as comparações entre tais elementos. tipos.

supercat
fonte