¿Podría una instancia ser igual a otra instancia de un tipo más específico?

25

He leído este artículo: Cómo escribir un método de igualdad en Java .

Básicamente, proporciona una solución para un método equals () que admite la herencia:

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

¿Pero es una buena idea? Estas dos instancias parecen ser iguales pero pueden tener dos códigos hash diferentes. ¿No está un poco mal?

Creo que esto se lograría mejor lanzando los operandos en su lugar.

Wes
fuente
1
El ejemplo con puntos de color como se da en el enlace tiene más sentido para mí. Consideraría que un punto 2D (x, y) puede verse como un punto 3D con un componente Z cero (x, y, 0), y me gustaría que la igualdad devuelva falso en su caso. De hecho, en el artículo, se dice explícitamente que un ColourPoint es diferente de un Punto y siempre devuelve falso.
coredump
10
Nada peor que los tutoriales que rompen las convenciones comunes ... Lleva años romper ese tipo de hábitos de los programadores.
corsiKa
3
@coredump Tratar un punto 2D como si tuviera una zcoordenada cero podría ser una convención útil para algunas aplicaciones (me vienen a la mente los primeros sistemas CAD que manejan datos heredados). Pero es una convención arbitraria. Los planos en espacios con 3 o más dimensiones pueden tener orientaciones arbitrarias ... es lo que hace interesantes los problemas interesantes.
Ben Rudgers
2
Está más que un poco mal .
Kevin Krumwiede

Respuestas:

71

Esto no debería ser igualdad porque rompe la transitividad . Considere estas dos expresiones:

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

Como la igualdad es transitiva, esto debería significar que la siguiente expresión también es verdadera:

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

Pero, por supuesto, no lo es.

Por lo tanto, su idea de conversión es correcta: espere que en Java, transmitir simplemente signifique emitir el tipo de referencia. Lo que realmente quieres aquí es un método de conversión que cree un nuevo Point2Dobjeto a partir de un Point3Dobjeto. Esto también haría que la expresión sea más significativa:

twoD.equals(threeD.projectXY())
Idan Arye
fuente
1
El artículo describe implementaciones que rompen la transitividad y ofrece una variedad de soluciones. En un dominio donde permitimos puntos 2D, ya hemos decidido que la tercera dimensión no importa. y entonces (10, 20, 50)igual (10, 20, 60)está bien. Solo nos importa 10y 20.
Ben Rudgers
1
¿Debería Point2Dtener un projectXYZ()método para proporcionar una Point3Drepresentación de sí mismo? En otras palabras, ¿las implementaciones deberían conocerse?
hjk
44
@hjk Deshacerse Point2Dparece más simple ya que proyectar puntos 2D requiere definir primero su plano en el espacio 3D. Si el punto 2D sabe que es plano, entonces ya es un punto 3D. Si no lo hace, no puede proyectarse. Me recuerda de Abbott Planilandia .
Ben Rudgers
@benrudgers Puede, sin embargo, definir un Plane3Dobjeto, que definirá un plano en el espacio 3D, ese plano puede tener un liftmétodo (2D-> 3D se levanta, no se proyecta) que aceptará un Point2Dy un número para el "tercer eje "- distancia del avión a lo largo del avión normal. Para facilitar su uso, puede definir los planos comunes como constantes estáticas, para que pueda hacer cosas comoPlane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye
@IdanArye Estaba comentando la sugerencia de que los puntos 2D deberían tener un método de proyección. En cuanto a los planos con métodos de elevación, creo que requeriría dos argumentos para tener sentido: un punto 2D y el plano en el que se supone que está, es decir, realmente necesita ser una proyección si no posee el punto ... y si posee el punto, ¿por qué no simplemente poseer un punto 3D y eliminar un tipo de datos problemático y el olor de un método erróneo? YMMV.
Ben Rudgers
10

Me alejo de leer el artículo pensando en la sabiduría de Alan J. Perlis:

Epigrama 9. Es mejor tener 100 funciones operando en una estructura de datos que 10 funciones en 10 estructuras de datos.

El hecho de que acertar con la "igualdad" es el tipo de problema que mantiene despierto a Martin Ordersky, inventor de Scala por la noche, debería hacer una pausa sobre si anular equalsen un árbol de herencia es una buena idea.

Lo que sucede cuando tenemos mala suerte ColoredPointes que nuestra geometría falla porque usamos la herencia para proliferar los tipos de datos en lugar de crear uno bueno. Esto a pesar de tener que regresar y modificar el nodo raíz del árbol de herencia para que equalsfuncione. ¿Por qué no basta con añadir una zy una colorpara Point?

La buena razón sería eso Pointy ColoredPointoperar en diferentes dominios ... al menos si esos dominios nunca se mezclan. Sin embargo, si ese es el caso, no necesitamos anular equals. Comparar ColoredPointy Pointpor igualdad solo tiene sentido en un tercer dominio donde se les permite mezclarse. Y en ese caso, probablemente sea mejor tener la "igualdad" adaptada a ese tercer dominio en lugar de tratar de aplicar la semántica de igualdad de uno u otro o de ambos dominios no mezclados. En otras palabras, la "igualdad" debe definirse localmente en el lugar donde tenemos el lodo que fluye desde ambos lados porque es posible que no queramos ColoredPoint.equals(pt)fallar contra las instancias, Pointincluso si el autor ColoredPointpensó que era una buena idea hace seis meses a las 2 a.m. .

ben rudgers
fuente
6

Cuando los antiguos dioses de la programación inventaban la programación orientada a objetos con clases, decidieron cuando se trataba de composición y herencia tener dos relaciones para un objeto: "es un" y "tiene un".
Esto resolvió parcialmente el problema de que las subclases fueran diferentes a las clases primarias, pero las hizo utilizables sin romper el código. Debido a que una instancia de subclase "es un" objeto de superclase y puede sustituirse directamente por él, a pesar de que la subclase tiene más funciones miembro o miembros de datos, el "tiene un" garantiza que realizará todas las funciones del padre y tendrá todos sus miembros. Entonces, se podría decir que un Point3D "es un" Punto, y un Point2D "es un" Punto si ambos heredan de Point. Además, un Point3D podría ser una subclase de Point2D.

Sin embargo, la igualdad entre clases es un problema específico del dominio, y el ejemplo anterior es ambiguo en cuanto a lo que el programador necesita para que el programa funcione correctamente. En general, se siguen las reglas del dominio matemático y los valores de los datos generarían igualdad si limita el alcance de la comparación a solo en este caso dos dimensiones, pero no si compara todos los miembros de datos.

Entonces obtienes una tabla de reducciones de igualdad:

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. 

Por lo general, elige las reglas más estrictas que pueda que aún realicen todas las funciones necesarias en su dominio problemático. Las pruebas de igualdad incorporadas para los números están diseñadas para ser tan restrictivas como pueden ser para fines matemáticos, pero el programador tiene muchas formas de evitarlo si ese no es el objetivo, incluido el redondeo arriba / abajo, truncamiento, gt, lt, etc. . Los objetos con marcas de tiempo a menudo se comparan por su tiempo de generación, por lo que cada instancia debe ser única para que las comparaciones sean muy específicas.

El factor de diseño en este caso es determinar formas eficientes de comparar objetos. A veces, una comparación recursiva de todos los miembros de datos de objetos es lo que debe hacer, y eso puede ser muy costoso si tiene muchos objetos con muchos miembros de datos. Las alternativas son solo comparar valores de datos relevantes, o hacer que el objeto genere un valor hash de sus miembros de datos interesados ​​para una comparación rápida con otros objetos similares, mantener las colecciones ordenadas y podadas para hacer comparaciones más rápidas y menos intensivas en CPU, y quizás permitir objetos que son idénticos en los datos que se seleccionarán y se colocará un puntero duplicado a un solo objeto en su lugar.

Chris Reid
fuente
2

La regla es, cada vez que anula hashcode(), anula equals(), y viceversa. Si esta es una buena idea o no, depende del uso previsto. Personalmente, usaría un método diferente ( isLike()o similar) para lograr el mismo efecto.

TMN
fuente
1
Puede estar bien anular hashCode sin anular iguales. Por ejemplo, uno haría eso para probar un algoritmo de hash diferente para la misma condición de igualdad.
Patricia Shanahan
1

A menudo es útil para personas que no se enfrentan al público clases tengan un método de prueba de equivalencia que permita que los objetos de diferentes tipos se consideren "iguales" si representan la misma información, pero debido a que Java no permite ningún medio por el cual las clases puedan hacerse pasar por cada uno. otro, a menudo es bueno tener un único tipo de contenedor orientado al público en los casos en que sea posible tener objetos equivalentes con diferentes representaciones.

Por ejemplo, considere una clase que encapsula una matriz de doublevalores 2D inmutable . Si un método externo solicita una matriz de identidad de tamaño 1000, un segundo solicita una matriz diagonal y pasa una matriz que contiene 1000 unidades, y un tercero pide una matriz 2D y pasa una matriz 1000x1000 donde los elementos en la diagonal primaria son todos 1.0 y todos los demás son cero, los objetos dados a las tres clases pueden usar diferentes almacenes de respaldo internamente [el primero tiene un solo campo para el tamaño, el segundo tiene una matriz de mil elementos y el tercero tiene mil matrices de 1000 elementos] pero deben informarse entre sí como equivalentes [ya que los tres encapsulan una matriz inmutable de 1000x1000 con unos en diagonal y ceros en cualquier otro lugar].

Más allá del hecho de que oculta la existencia de distintos tipos de tiendas de respaldo, el contenedor también será útil para facilitar las comparaciones, ya que la verificación de la equivalencia de los artículos generalmente será un proceso de varios pasos. Pregunte al primer elemento si sabe si es igual al segundo; si no sabe, pregunte al segundo si sabe si es igual al primero. Si ninguno de los objetos lo sabe, pregúntele a cada matriz sobre el contenido de sus elementos individuales [uno podría agregar otras comprobaciones antes de decidir hacer la ruta de comparación de elementos individuales larga y lenta].

Tenga en cuenta que el método de prueba de equivalencia para cada objeto en este escenario necesitaría devolver un valor de tres estados ("Sí, soy equivalente", "No, no soy equivalente" o "No sé"), entonces el método normal "igual" no sería adecuado. Si bien cualquier objeto podría simplemente responder "No sé" cuando se le pregunta sobre cualquier otro, agregar lógica a, por ejemplo, una matriz diagonal que no molestaría en preguntar a ninguna matriz de identidad o matriz diagonal sobre cualquier elemento fuera de la diagonal principal, aceleraría enormemente las comparaciones entre tales tipos.

Super gato
fuente