La respuesta de Kilian Foth es excelente. Solo me gustaría agregar el ejemplo canónico * de por qué esto es un problema. Imagine una clase de punto entero:
class Point2D {
public int x;
public int y;
// constructor
public Point2D(int theX, int theY) { x = theX; y = theY; }
public int hashCode() { return x + y; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point2D) ) { return false; }
Point2D that = (Point2D) o;
return (x == that.x) &&
(y == that.y);
}
}
Ahora subclasifiquemos para que sea un punto 3D.
class Point3D extends Point2D {
public int z;
// constructor
public Point3D(int theX, int theY, int theZ) {
super(x, y); z = theZ;
}
public int hashCode() { return super.hashCode() + z; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point3D) ) { return false; }
Point3D that = (Point3D) o;
return super.equals(that) &&
(z == that.z);
}
}
Súper simple! Usemos nuestros puntos:
Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);
p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false
Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);
p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
Probablemente se esté preguntando por qué estoy publicando un ejemplo tan fácil. Aquí está el truco:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Cuando comparamos el punto 2D con el punto 3D equivalente, obtenemos verdadero, pero cuando invertimos la comparación, obtenemos falso (porque p2a falla instanceof Point3D
).
Conclusión
Por lo general, es posible implementar un método en una subclase de tal manera que ya no sea compatible con la forma en que la superclase espera que funcione.
En general, es imposible implementar equals () en una subclase significativamente diferente de una manera que sea compatible con su clase padre.
Cuando escribe una clase que pretende permitir que las personas subclasifiquen, es una muy buena idea redactar un contrato sobre cómo debe comportarse cada método. Aún mejor sería un conjunto de pruebas unitarias que las personas podrían ejecutar contra sus implementaciones de métodos anulados para demostrar que no violan el contrato. Casi nadie hace eso porque es demasiado trabajo. Pero si te importa, eso es lo que debes hacer.
Un gran ejemplo de un contrato bien enunciado es Comparator . Simplemente ignore lo que dice .equals()
por las razones descritas anteriormente. Aquí hay un ejemplo de cómo Comparator puede hacer cosas .equals()
que no puede .
Notas
El elemento 8 "Java efectivo" de Josh Bloch fue la fuente de este ejemplo, pero Bloch usa un ColorPoint que agrega un color en lugar de un tercer eje y usa dobles en lugar de ints. El ejemplo de Bloch en Java está básicamente duplicado por Odersky / Spoon / Venners que hicieron que su ejemplo estuviera disponible en línea.
Varias personas se han opuesto a este ejemplo porque si le informa a la clase principal sobre la subclase, puede solucionar este problema. Eso es cierto si hay un número suficientemente pequeño de subclases y si el padre las conoce todas. Pero la pregunta original era sobre hacer una API para la cual alguien más escribirá subclases. En ese caso, generalmente no puede actualizar la implementación principal para que sea compatible con las subclases.
Prima
Comparator también es interesante porque soluciona el problema de implementar equals () correctamente. Mejor aún, sigue un patrón para solucionar este tipo de problema de herencia: el patrón de diseño de la Estrategia. Las clases de tipo que entusiasman a la gente de Haskell y Scala también son el patrón de estrategia. La herencia no es mala o incorrecta, solo es complicada. Para leer más, consulte el artículo de Philip Wadler Cómo hacer que el polimorfismo ad-hoc sea menos ad-hoc.