Java8: Dlaczego zabronione jest definiowanie domyślnej metody dla metody z java.lang.Object

133

Metody domyślne to fajne nowe narzędzie w naszym zestawie narzędzi Java. Próbowałem jednak napisać interfejs, który definiuje defaultwersję toStringmetody. Java mówi mi, że jest to zabronione, ponieważ metody zadeklarowane w java.lang.Objectnie mogą być defaultedytowane. Dlaczego tak się dzieje?

Wiem, że istnieje zasada "klasa bazowa zawsze wygrywa", więc domyślnie (kalambur;) każda defaultimplementacja Objectmetody zostanie nadpisana przez metodę z i Objecttak. Jednak nie widzę powodu, dla którego nie powinno być wyjątku dla metod z Objectspecyfikacji. Szczególnie toStringdlatego, że domyślna implementacja może być bardzo przydatna.

Więc jaki jest powód, dla którego projektanci Javy zdecydowali się nie zezwalać defaultmetodom na przesłanianie metod z Object?

geksycyd
źródło
1
Czuję się teraz tak dobrze ze sobą, głosując za tym 100 razy, a tym samym otrzymuję złotą odznakę. dobre pytanie!
Eugene,

Odpowiedzi:

188

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 AbstractListw 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ąć AbstractListprzykład; byłoby wspaniale, gdybyśmy mogli się go pozbyć AbstractListi umieścić to zachowanie w Listinterfejsie. Ale kiedy wyjdziesz poza ten oczywisty przykład, nie ma wielu innych dobrych przykładów. W katalogu głównym AbstractListjest 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 Foospojrzenia 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ą equalsimplementację. Ups! Teraz semantyka Foozostał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.

Brian Goetz
źródło
14
Cieszę się, że poświęciłeś czas na wyjaśnienie tego i doceniam wszystkie rozważane czynniki. Zgadzam się, że byłoby to niebezpieczne dla hashCodei equals, ale myślę, że byłoby to bardzo przydatne dla toString. Na przykład, niektóre Displayableinterfejsu może określić String display()metodą, i zaoszczędzić tonę boilerplate móc określić default String toString() { return display(); }w Displayable, zamiast konieczności każdy Displayabledo realizacji toString()lub przedłużenie DisplayableToStringklasę bazową.
Brandon,
8
@Brandon Masz rację, że zezwolenie na dziedziczenie toString () nie byłoby niebezpieczne w taki sam sposób, jak byłoby to w przypadku equals () i hashCode (). Z drugiej strony, teraz ta funkcja byłaby jeszcze bardziej nieregularna - i nadal ponosiłbyś tę samą dodatkową złożoność dotyczącą reguł dziedziczenia, ze względu na tę jedną metodę ... wydaje się, że lepiej jest wyraźnie nakreślić linię tam, gdzie to zrobiliśmy .
Brian Goetz
5
@gexicide Jeśli toString()opiera się tylko na metodach interfejsu, możesz po prostu dodać coś podobnego default 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 jak Objects.hash(), Arrays.deepEquals()i Arrays.deepToString(). +1 dla odpowiedzi @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-
3
Domyślne zachowanie metody toString () wyrażenia lambda jest naprawdę nieprzyjemne. Wiem, że fabryka lambda została zaprojektowana tak, aby była bardzo prosta i szybka, ale wypluwanie odważnej nazwy klasy naprawdę nie jest pomocne. Posiadanie 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.
Groostav
Każda zmiana w toString w dowolnej klasie, podklasie, elemencie instancji może mieć taki wpływ na implementację klas lub użytkowników klasy. Co więcej, każda zmiana którejkolwiek z metod domyślnych prawdopodobnie wpłynęłaby również na wszystkie klasy implementujące. Więc co jest takiego specjalnego w toString, hashCode, jeśli chodzi o kogoś, kto zmienia zachowanie interfejsu? Jeśli klasa rozszerza inną klasę, mogą zmienić to samo. Lub jeśli używają wzorca delegowania. Ktoś korzystający z interfejsów Java 8 będzie musiał to zrobić poprzez aktualizację. Mogło zostać dostarczone ostrzeżenie / błąd, który można ukryć w podklasie.
mmm
30

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 w java.lang.Objectmiał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 .

jarnbjo
źródło
3

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.

Marwin
źródło
Nie zauważyłem drugiego akapitu odpowiedzi gexicide podczas wysyłania mojej. Zawiera odsyłacz wyjaśniający bardziej szczegółowo problem.
Marwin
1

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.

Kumar Abhishek
źródło
1

Aby udzielić bardzo pedantycznej odpowiedzi, zabronione jest jedynie definiowanie defaultmetody dla metody publicznej z java.lang.Object. Aby odpowiedzieć na to pytanie, należy rozważyć 11 metod, które można podzielić na trzy kategorie.

  1. Sześć Objectmetod nie może mieć defaultmetod, ponieważ są finali nie mogą być w ogóle nadpisane:getClass() , notify(), notifyAll(), wait(), wait(long), i wait(long, int).
  2. Trzy z Object metod nie można mieć defaultmetody powodów podanych powyżej Brian Goetz: equals(Object), hashCode(), i toString().
  3. Dwie Objectmetody mogą mieć defaultmetody, chociaż wartość takich wartości domyślnych jest co najmniej wątpliwa: clone()i finalize().

    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();
            }
        }
    }
    
jaco0646
źródło
.Jaki jest sens? Wciąż nie odpowiedziałeś na część DLACZEGO - „Więc jaki jest powód, dla którego projektanci Java zdecydowali się nie zezwalać na domyślne metody zastępujące metody z Object?”
pro_cheats