To kolejny z tych problemów z projektowaniem języka, który wydaje się „oczywiście dobrym pomysłem”, dopóki nie zaczniesz kopać i nie zdasz sobie sprawy, że to naprawdę zły pomysł.
W tej wiadomości jest dużo na ten temat (i na inne tematy). Było kilka sił projektowych, które zbiegły się, aby doprowadzić nas do obecnego projektu:
- Chęć utrzymania prostego modelu dziedziczenia;
- Fakt, że po przejrzeniu oczywistych przykładów (np. Zamiany
AbstractList
w interfejs), zdajesz sobie sprawę, że dziedziczenie równości / hashCode / toString jest silnie powiązane z pojedynczym dziedziczeniem i stanem, a interfejsy są dziedziczone wielokrotnie i bezstanowe;
- Że potencjalnie otworzył drzwi do pewnych zaskakujących zachowań.
Dotknąłeś już celu „nie komplikuj”; reguły dziedziczenia i rozwiązywania konfliktów są zaprojektowane tak, aby były bardzo proste (klasy wygrywają z interfejsami, interfejsy pochodne wygrywają z superinterfejsami, a wszelkie inne konflikty są rozwiązywane przez klasę implementującą). Oczywiście reguły te można zmodyfikować, aby zrobić wyjątek, ale Myślę, że kiedy zaczniesz pociągać za sznurek, przekonasz się, że przyrostowa złożoność nie jest tak mała, jak mogłoby się wydawać.
Oczywiście istnieje pewien stopień korzyści, który uzasadniałby większą złożoność, ale w tym przypadku go nie ma. Metody, o których tutaj mówimy, to equals, hashCode i toString. Wszystkie te metody są z natury rzeczy związane ze stanem obiektu i to klasa, która jest właścicielem stanu, a nie interfejs, jest w najlepszej pozycji do określenia, co oznacza równość dla tej klasy (zwłaszcza, że kontrakt równości jest dość silny; patrz Effective Java z zaskakującymi konsekwencjami); twórcy interfejsów są po prostu zbyt daleko.
Łatwo jest wyciągnąć AbstractList
przykład; byłoby wspaniale, gdybyśmy mogli się go pozbyć AbstractList
i umieścić to zachowanie w List
interfejsie. Ale kiedy wyjdziesz poza ten oczywisty przykład, nie ma wielu innych dobrych przykładów. W katalogu głównym AbstractList
jest przeznaczony do dziedziczenia pojedynczego. Ale interfejsy muszą być zaprojektowane do wielokrotnego dziedziczenia.
Ponadto wyobraź sobie, że piszesz te zajęcia:
class Foo implements com.libraryA.Bar, com.libraryB.Moo {
}
Te Foo
spojrzenia pisarz za supertypes, nie widzi realizację równych, i stwierdza, że aby uzyskać równość odniesienia, wszyscy musimy zrobić, to dziedziczą równi z Object
. Następnie, w przyszłym tygodniu, opiekun biblioteki Baru „z pomocą” dodaje domyślną equals
implementację. Ups! Teraz semantyka Foo
została zerwana przez interfejs w innej domenie konserwacyjnej, który „z pomocą” dodał domyślną dla wspólnej metody.
Domyślne mają być domyślne. Dodanie wartości domyślnej do interfejsu, w którym jej nie było (w dowolnym miejscu w hierarchii), nie powinno wpływać na semantykę konkretnych klas implementujących. Ale gdyby wartości domyślne mogły „przesłonić” metody Object, nie byłoby to prawdą.
Tak więc, chociaż wydaje się to nieszkodliwą cechą, w rzeczywistości jest dość szkodliwa: dodaje wiele złożoności dla małej przyrostowej ekspresji i sprawia, że jest zbyt łatwe dla dobrze zamierzonych, nieszkodliwie wyglądających zmian w oddzielnie skompilowanych interfejsach, aby podważyć zamierzona semantyka zajęć realizacyjnych.