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 eseguiamo la sottoclasse in modo che diventi 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, otteniamo false (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 super-classe si aspetta che funzioni.
-
In genere è impossibile implementare equals () in una sottoclasse significativamente diversa in un modo compatibile con la sua classe genitore.
Quando scrivi una classe che intendi consentire alle persone di sottoclasse, è davvero una buona idea scrivere un contratto su come ogni metodo dovrebbe comportarsi. Ancora meglio sarebbe una serie di test unitari che le persone potrebbero eseguire contro le loro implementazioni dei metodi sovrascritti 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 un contratto ben definito è Comparatore . Ignora semplicemente cosa dice su .equals()
per i motivi sopra descritti. Ecco un esempio di come il comparatore può fare cose .equals()
non può .
Note
-
L'articolo 8 di Josh Bloch "Effective Java" era la fonte di questo esempio, ma Bloch usa un ColorPoint che aggiunge un colore al posto del terzo asse e usa il doppio invece degli interi. L'esempio di Bloch in Java è sostanzialmente duplicato da Odersky / Spoon / Venners che ha reso il loro esempio disponibile online.
-
Diverse persone hanno obiettato a questo esempio perché se si consente alla classe genitrice di conoscere la sottoclasse, è possibile risolvere questo problema. Questo è vero se c'è un numero abbastanza piccolo di sottoclassi e se il genitore ne sa tutto. Ma la domanda iniziale riguardava la creazione di un'API per la quale qualcun altro scriverà sottoclassi. In questo caso, generalmente non è possibile aggiornare l'implementazione genitore per essere compatibile con le sottoclassi.
Bonus
Il comparatore è anche interessante perché funziona correttamente sulla questione dell'implementazione di equals (). Meglio ancora, segue uno schema per risolvere questo tipo di problema dell'ereditarietà: il modello di progettazione della strategia. I Typeclasses che le persone di Haskell e Scala sono entusiaste sono anche il modello di strategia. L'ereditarietà non è male o sbagliata, è solo complicata. Per ulteriori informazioni, consultare il documento di Philip Wadler Come per rendere il polimorfismo ad-hoc meno ad hoc