Questo è un altro di quei problemi di design del linguaggio che sembra "ovviamente una buona idea" fino a quando non inizi a scavare e ti rendi conto che in realtà è una cattiva idea.
Questa mail ha molto sull'argomento (e anche su altri argomenti). Ci sono state diverse forze di progettazione che sono confluite per portarci al design attuale:
- Il desiderio di mantenere semplice il modello ereditario;
- Il fatto che una volta esaminati gli esempi ovvi (ad esempio, trasformandosi
AbstractList
in un'interfaccia), ti rendi conto che ereditare equals / hashCode / toString è fortemente legato alla singola eredità e stato e che le interfacce sono moltiplicate ereditate e apolidi;
- Che potenzialmente ha aperto la porta ad alcuni comportamenti sorprendenti.
Hai già toccato l'obiettivo "Keep it simple"; le regole di ereditarietà e risoluzione dei conflitti sono progettate per essere molto semplici (le classi vincono sulle interfacce, le interfacce derivate vincono sulle superinterfacce e qualsiasi altro conflitto è risolto dalla classe di implementazione.) Naturalmente queste regole potrebbero essere modificate per fare un'eccezione, ma Penso che scoprirai quando inizi a tirare quella stringa, che la complessità incrementale non è così piccola come potresti pensare.
Certo, c'è un certo grado di beneficio che giustificherebbe una maggiore complessità, ma in questo caso non è lì. I metodi di cui stiamo parlando sono uguali, hashCode e toString. Questi metodi sono tutti intrinsecamente relativi allo stato dell'oggetto, ed è la classe che possiede lo stato, non l'interfaccia, che si trova nella posizione migliore per determinare cosa significhi l'uguaglianza per quella classe (specialmente perché il contratto per l'uguaglianza è abbastanza forte; vedi Efficace Java per alcune conseguenze sorprendenti); gli scrittori di interfacce sono troppo lontani.
È facile estrarre l' AbstractList
esempio; sarebbe bello se potessimo sbarazzarci di AbstractList
e mettere il comportamento List
nell'interfaccia. Ma una volta superato questo ovvio esempio, non ci sono molti altri buoni esempi da trovare. Alla radice, AbstractList
è progettato per singola eredità. Ma le interfacce devono essere progettate per l'ereditarietà multipla.
Inoltre, immagina di scrivere questa lezione:
class Foo implements com.libraryA.Bar, com.libraryB.Moo {
// Implementation of Foo, that does NOT override equals
}
Lo Foo
scrittore osserva i supertipi, non vede l'implementazione di uguali e conclude che per ottenere l'uguaglianza di riferimento, tutto ciò che deve fare è ereditare uguali Object
. Quindi, la settimana prossima, il manutentore della libreria per la barra "utile" aggiunge equals
un'implementazione predefinita . Ops! Ora la semantica di Foo
è stata interrotta da un'interfaccia in un altro dominio di manutenzione "utile" aggiungendo un valore predefinito per un metodo comune.
I valori predefiniti dovrebbero essere predefiniti. L'aggiunta di un valore predefinito a un'interfaccia in cui non era presente (in qualsiasi punto della gerarchia) non dovrebbe influire sulla semantica delle classi di implementazione concrete. Ma se i valori predefiniti potessero "sovrascrivere" i metodi Object, ciò non sarebbe vero.
Quindi, sebbene sembri una funzionalità innocua, in realtà è piuttosto dannosa: aggiunge molta complessità per una piccola espressività incrementale e lo rende troppo facile per modifiche minacciose e dall'aspetto innocuo di interfacce compilate separatamente per minare la semantica prevista delle classi di attuazione.