La risposta di Kilian Foth è eccellente. Vorrei solo aggiungere l'esempio canonico * del perché questo è un problema. Immagina una classe Point intera:
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);
}
}
Ora suddividiamo la sottoclasse in 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);
}
}
Super semplice! Usiamo i nostri punti:
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
Probabilmente ti starai chiedendo perché sto pubblicando un esempio così semplice. Ecco il trucco:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Quando confrontiamo il punto 2D con il punto 3D equivalente, diventiamo veri, ma quando invertiamo il confronto, diventiamo falsi (perché p2a fallisce instanceof Point3D
).
Conclusione
Di solito è possibile implementare un metodo in una sottoclasse in modo tale che non sia più compatibile con il modo in cui la superclasse si aspetta che funzioni.
È generalmente impossibile implementare equals () su una sottoclasse significativamente diversa in un modo compatibile con la sua classe genitore.
Quando scrivi una classe che intendi consentire alle persone di sottoclassare, è davvero una buona idea scrivere un contratto su come ciascun metodo dovrebbe comportarsi. Ancora meglio sarebbe un insieme di unit test che le persone potrebbero eseguire contro l'implementazione di metodi scavalcati per dimostrare che non violano il contratto. Quasi nessuno lo fa perché è troppo lavoro. Ma se ti interessa, questa è la cosa da fare.
Un ottimo esempio di contratto ben definito è Comparator . Basta ignorare ciò che dice .equals()
per i motivi sopra descritti. Ecco un esempio di come Comparator può fare le cose .equals()
no .
Gli appunti
La voce 8 di "Effective Java" di Josh Bloch è stata la fonte di questo esempio, ma Bloch utilizza un ColorPoint che aggiunge un colore anziché un terzo asse e utilizza doppi invece di ints. L'esempio Java di Bloch è sostanzialmente duplicato da Odersky / Spoon / Venners che ha reso disponibile il loro esempio online.
Diverse persone hanno obiettato a questo esempio perché se si fa sapere alla classe genitrice della sottoclasse, è possibile risolvere questo problema. Questo è vero se esiste un numero sufficiente di sottoclassi e se il genitore ne è a conoscenza. Ma la domanda originale era di creare un'API per la quale qualcun altro scriverà sottoclassi. In tal caso, in genere non è possibile aggiornare l'implementazione padre in modo che sia compatibile con le sottoclassi.
indennità
Comparator è anche interessante perché risolve correttamente la questione dell'implementazione di equals (). Meglio ancora, segue un modello per risolvere questo tipo di problema di eredità: il modello di progettazione della strategia. Le Typeclass di cui le persone di Haskell e Scala si entusiasmano sono anche il modello di strategia. L'ereditarietà non è né cattiva né sbagliata, è solo complicata. Per ulteriori approfondimenti, consulta l'articolo di Philip Wadler Come rendere il polimorfismo ad hoc meno ad hoc