Uguaglianza ed ereditarietà¶
Questa breve nota discute i limiti imposti dall'ereditarietà al soddisfacimento del contratto imposto dal metodo equals
) della classe Object
.
La nota segue l'esposizione della Sezione 7.9.3 del testo "Program Development in Java" (di Barbara H. Liskov et al.) e dell'Item 10 del testo "Effective Java" (di Joshua Bloch).
Simmetria¶
L'implementazione ovvia non funziona: se un sottotipo aggiunge un "valore", non basta controllare con instanceof
e verificare che siano uguali i valori in gioco…
Consideriamo il seguente tipo T
e il suo sottotipo S
Entrambi i tipi sono dotati di una implementazione di equals
(qui evidenziata in giallo) che, dopo aver verificato il tipo dell'argomento obj
procedono all'ovvio controllo dell'identità dei valori del/i campo/i corrispondente/i.
Tale implementazione viola la proprietà di simmetria, ossia $t \sim s$ non implica $s \sim t$, come dimostrato dal seguente metodo main
che produce l'output
Quel che accade è che t instanceof S
è falso, ma s instanceof T
è vero, per cui l'implementazione di equals
di S
non sa cosa fare con t
, mentre quella di T
funziona con s
(ovviamente trascurando b
, di cui ignora l'esistenza).
Transitività¶
Non è facile aggiustare l'implementazione di equals
di S
gestendo accortamente il caso in cui obj
ha il tipo effettivo di T
imitando il suo comportamento, ossia "scordandosi" di b
, come (nella parte evidenziata) nel seguente codice
Così facendo infatti, la simmetria è rispettata, ma risulterà violata la proprietà transitiva, ovvero anche avendo $s \sim t$ e $t \sim u$ non è detto che $s \sim u$. Questo è dimostrato dalla seguente porzione di codice
che produce l'output
Il principio di sostituzione¶
Si potrebbe pensare che il problema sia instanceof
che è antisimmetrico, per cui una soluzione che viene talvolta suggerita è di sostituire nell'implementazione di equals
di T
la condizione obj instanceof T
con la condizione getClass() == obj.getClass()
che è simmetrica (e risulta vera se e solo se gli oggetti hanno identico tipo concreto — attenzione, però, occorre occpuarsi esplicitamente del caso obj == null
).
Si può applicare la stessa scelta anche a S
, sebbene questo sia utile solo qualora tale classe venisse estesa.
In questo modo la simmetria e la transitività sono rispettate perché equals
funziona solo per oggetti del medesimo tipo, avendo valore false
per oggetti di tipo diverso (anche se sottotipi l'uno dell'altro).
Questo però viola il principio di sostituzione!
Per cogliere meglio questo aspetto consideriamo R
, una ulteriore estensione di T
che non aggiunga alcun "valore", ma solo un "comportamento" e che, pertanto, non ridefinisca neppure equals
.
Supponiamo ora di sviluppare una classe di utilità con un metodo isSmall
che restituisca true
per tutti gli oggetti per cui a
è 1
, oppure 2
e supponiamo (sebbene questo sia estremamente innaturale e inefficiente) di implementare tale metodo verificando l'appartenenza a un insieme SMALLS
che contiene gli unici due oggetti che soddisfano tale requisito.
La seguente porzione di codice mostra che il confronto tra oggetti di tipo diverso (anche se uno è sottotipo dell'altro) restituisce false
(il che risolve i problemi di simmetria e transitività); mostra però anche che sostituendo il sottotipo R
a T
(vedi il codice evidenziato) il comportamento cambia!
L'output infatti è
Questo accade perché l'implementazione di Set
si basa sul metodo equals
per decidere se un oggetto appartiene all'insieme, ma gli elementi di SMALLS
(che sono di tipo T
) non risultano mai uguali a oggetti di tipo R
!