Metody domyślne to fajne nowe narzędzie w naszym zestawie narzędzi Java. Próbowałem jednak napisać interfejs, który definiuje default
wersję toString
metody. Java mówi mi, że jest to zabronione, ponieważ metody zadeklarowane w java.lang.Object
nie mogą być default
edytowane. Dlaczego tak się dzieje?
Wiem, że istnieje zasada "klasa bazowa zawsze wygrywa", więc domyślnie (kalambur;) każda default
implementacja Object
metody zostanie nadpisana przez metodę z i Object
tak. Jednak nie widzę powodu, dla którego nie powinno być wyjątku dla metod z Object
specyfikacji. Szczególnie toString
dlatego, że domyślna implementacja może być bardzo przydatna.
Więc jaki jest powód, dla którego projektanci Javy zdecydowali się nie zezwalać default
metodom na przesłanianie metod z Object
?
źródło
Odpowiedzi:
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:
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;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 wList
interfejsie. Ale kiedy wyjdziesz poza ten oczywisty przykład, nie ma wielu innych dobrych przykładów. W katalogu głównymAbstractList
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 { // Implementation of Foo, that does NOT override equals }
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 zObject
. Następnie, w przyszłym tygodniu, opiekun biblioteki Baru „z pomocą” dodaje domyślnąequals
implementację. Ups! Teraz semantykaFoo
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.
źródło
hashCode
iequals
, ale myślę, że byłoby to bardzo przydatne dlatoString
. Na przykład, niektóreDisplayable
interfejsu może określićString display()
metodą, i zaoszczędzić tonę boilerplate móc określićdefault String toString() { return display(); }
wDisplayable
, zamiast konieczności każdyDisplayable
do realizacjitoString()
lub przedłużenieDisplayableToString
klasę bazową.toString()
opiera się tylko na metodach interfejsu, możesz po prostu dodać coś podobnegodefault String toStringImpl()
do interfejsu i nadpisaćtoString()
w każdej podklasie, aby wywołać implementację interfejsu - trochę brzydkie, ale działa i lepsze niż nic. :) Innym sposobem jest zrobienie czegoś takiego jakObjects.hash()
,Arrays.deepEquals()
iArrays.deepToString()
. +1 dla odpowiedzi @ BrianGoetz!default toString()
nadpisanie w interfejsie funkcjonalnego pozwoli nam --at przynajmniej: zrobić coś wypluć podpisu funkcji i klasy dominującej realizatora. Jeszcze lepiej, gdybyśmy mogli zastosować pewne rekurencyjne strategie toString, moglibyśmy przejść przez zamknięcie, aby uzyskać naprawdę dobry opis lambda, a tym samym drastycznie poprawić krzywą uczenia się lambda.Zabrania się definiowania domyślnych metod w interfejsach dla metod w
java.lang.Object
, ponieważ domyślne metody nigdy nie byłyby „osiągalne”.Domyślne metody interfejsu można nadpisać w klasach implementujących interfejs, a implementacja metody w klasie ma wyższy priorytet niż implementacja interfejsu, nawet jeśli metoda jest zaimplementowana w nadklasie. Ponieważ wszystkie klasy dziedziczą po
java.lang.Object
, metody wjava.lang.Object
miałyby pierwszeństwo przed domyślną metodą w interfejsie i zamiast tego byłyby wywoływane.Brian Goetz z firmy Oracle przedstawia więcej szczegółów na temat decyzji projektowej w tym poście na liście mailingowej .
źródło
Nie zaglądam do głowy autorów języka Java, więc możemy się tylko domyślać. Ale widzę wiele powodów i całkowicie się z nimi zgadzam w tej kwestii.
Głównym powodem wprowadzenia metod domyślnych jest możliwość dodawania nowych metod do interfejsów bez naruszania wstecznej kompatybilności starszych implementacji. Metody domyślne mogą również służyć do dostarczania metod „wygodnych” bez konieczności ich definiowania w każdej z klas implementujących.
Żadne z nich nie ma zastosowania do toString i innych metod Object. Mówiąc najprościej, domyślne metody zostały zaprojektowane, aby zapewnić domyślne zachowania, w przypadku gdy nie ma innej definicji. Nie dostarczać implementacji, które będą „konkurować” z innymi istniejącymi implementacjami.
Zasada „klasa bazowa zawsze wygrywa” ma również swoje solidne powody. Przypuszcza się, że klasy definiują rzeczywiste implementacje, podczas gdy interfejsy definiują domyślne implementacje, które są nieco słabsze.
Ponadto wprowadzanie JAKICHKOLWIEK wyjątków od ogólnych zasad powoduje niepotrzebną złożoność i rodzi inne pytania. Obiekt jest (mniej więcej) klasą jak każda inna, więc dlaczego miałby zachowywać się inaczej?
Ogólnie rzecz biorąc, rozwiązanie, które proponujesz, prawdopodobnie przyniosłoby więcej wad niż zalet.
źródło
Rozumowanie jest bardzo proste, ponieważ Object jest klasą bazową dla wszystkich klas Java. Więc nawet jeśli mamy metodę Object zdefiniowaną jako domyślną w jakimś interfejsie, będzie ona bezużyteczna, ponieważ metoda Object będzie zawsze używana. Dlatego, aby uniknąć nieporozumień, nie możemy mieć metod domyślnych, które przesłaniają metody klasy Object.
źródło
Aby udzielić bardzo pedantycznej odpowiedzi, zabronione jest jedynie definiowanie
default
metody dla metody publicznej zjava.lang.Object
. Aby odpowiedzieć na to pytanie, należy rozważyć 11 metod, które można podzielić na trzy kategorie.Object
metod nie może miećdefault
metod, ponieważ sąfinal
i nie mogą być w ogóle nadpisane:getClass()
,notify()
,notifyAll()
,wait()
,wait(long)
, iwait(long, int)
.Object
metod nie można miećdefault
metody powodów podanych powyżej Brian Goetz:equals(Object)
,hashCode()
, itoString()
.Dwie
Object
metody mogą miećdefault
metody, chociaż wartość takich wartości domyślnych jest co najmniej wątpliwa:clone()
ifinalize()
.public class Main { public static void main(String... args) { new FOO().clone(); new FOO().finalize(); } interface ClonerFinalizer { default Object clone() {System.out.println("default clone"); return this;} default void finalize() {System.out.println("default finalize");} } static class FOO implements ClonerFinalizer { @Override public Object clone() { return ClonerFinalizer.super.clone(); } @Override public void finalize() { ClonerFinalizer.super.finalize(); } } }
źródło