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
AbstractListin 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' AbstractListesempio; sarebbe bello se potessimo sbarazzarci di AbstractListe mettere il comportamento Listnell'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 Fooscrittore 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 equalsun'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.