Dlaczego do interfejsów w Javie 8 dodano domyślne i statyczne metody, skoro mieliśmy już klasy abstrakcyjne?


99

W Javie 8 interfejsy mogą zawierać zaimplementowane metody, metody statyczne i tak zwane metody „domyślne” (których klasy implementujące nie muszą zastępować).

W mojej (prawdopodobnie naiwnej) opinii nie było potrzeby naruszania takich interfejsów. Interfejsy zawsze były umową, którą musisz wypełnić, a jest to bardzo prosta i czysta koncepcja. Teraz jest to połączenie kilku rzeczy. W mojej opinii:

  1. metody statyczne nie należą do interfejsów. Należą do klas użytkowych.
  2. „domyślne” metody nie powinny być w ogóle dozwolone w interfejsach. W tym celu zawsze można użyć klasy abstrakcyjnej.

W skrócie:

Przed Javą 8:

  • Możesz użyć klas abstrakcyjnych i regularnych, aby zapewnić metody statyczne i domyślne. Rola interfejsów jest jasna.
  • Wszystkie metody interfejsu powinny zostać zastąpione przez implementację klas.
  • Nie można dodać nowej metody do interfejsu bez modyfikacji wszystkich implementacji, ale tak naprawdę jest to dobra rzecz.

Po Javie 8:

  • Praktycznie nie ma różnicy między interfejsem a klasą abstrakcyjną (inną niż wielokrotne dziedziczenie). W rzeczywistości możesz emulować zwykłą klasę za pomocą interfejsu.
  • Podczas programowania implementacji programiści mogą zapomnieć o przesłonięciu metod domyślnych.
  • Wystąpił błąd kompilacji, jeśli klasa próbuje zaimplementować dwa lub więcej interfejsów mających domyślną metodę o tej samej sygnaturze.
  • Dodając domyślną metodę do interfejsu, każda klasa implementująca automatycznie dziedziczy to zachowanie. Niektóre z tych klas mogły nie zostać zaprojektowane z myślą o tej nowej funkcjonalności, co może powodować problemy. Na przykład, jeśli ktoś doda nową domyślną metodę default void foo()do interfejsu Ix, wówczas klasa Cximplementująca Ixi posiadająca prywatną foometodę z tym samym podpisem nie zostanie skompilowana.

Jakie są główne przyczyny tak poważnych zmian i jakie nowe korzyści (jeśli w ogóle) dodają?


30
Pytanie dodatkowe: Dlaczego zamiast tego nie wprowadzili wielokrotnego dziedziczenia klas?

2
metody statyczne nie należą do interfejsów. Należą do klas użytkowych. Nie, należą do @Deprecatedkategorii! metody statyczne są jedną z najbardziej nadużywanych konstrukcji w Javie z powodu ignorancji i lenistwa. Wiele metod statycznych zwykle oznacza niekompetentny programator, zwiększenie sprzężenia o kilka rzędów wielkości i jest koszmarem dla testów jednostkowych i refaktoryzacji, gdy zdasz sobie sprawę, dlaczego są złym pomysłem!

11
@JarrodRoberson Czy możesz podać więcej wskazówek (linki byłyby świetne) na temat „czy koszmar do testów jednostkowych i refaktoryzacji, gdy zdasz sobie sprawę, dlaczego są złym pomysłem!”? Nigdy tak nie myślałem i chciałbym dowiedzieć się więcej na ten temat.
wodnisty

11
@Chris Wielokrotne dziedziczenie stanu powoduje szereg problemów, szczególnie przy alokacji pamięci ( klasyczny problem z diamentem ). Wielokrotne dziedziczenie zachowania zależy jednak tylko od realizacji spełniającej umowę już ustaloną przez interfejs (interfejs może wywoływać inne metody, które deklaruje i wymaga). Subtelne, ale bardzo interesujące wyróżnienie.
ssube

1
chcesz dodać metodę do istniejącego interfejsu, było nieporęczne lub prawie niemożliwe przed java8 teraz możesz po prostu dodać ją jako metodę domyślną.
VdeX,

Odpowiedzi:


59

Dobrym motywującym przykładem domyślnych metod jest standardowa biblioteka Java, w której jest teraz

list.sort(ordering);

zamiast

Collections.sort(list, ordering);

Nie sądzę, aby mogli zrobić to inaczej bez więcej niż jednej identycznej implementacji List.sort.


19
C # rozwiązuje ten problem dzięki metodom rozszerzenia.
Robert Harvey

5
i pozwala połączonej liście na użycie dodatkowej przestrzeni O (1) i O (n log n) scalania czasu, ponieważ połączone listy mogą zostać scalone w miejscu, w java 7 zrzuca do zewnętrznej tablicy, a następnie sortuje
maniakiem zapadkowym

5
Znalazłem ten artykuł, w którym Goetz wyjaśnia problem. Na razie zaznaczę tę odpowiedź jako rozwiązanie.
Pan Smith

1
@RobertHarvey: Utwórz listę o wartości dwustu milionów pozycji <Bajt>, użyj, IEnumerable<Byte>.Appendaby do nich dołączyć, a następnie zadzwoń Count, a następnie powiedz mi, jak metody rozszerzenia rozwiązują problem. Gdyby CountIsKnowni Countbyli członkami IEnumerable<T>, powrót z Appendmógłby reklamować, CountIsKnowngdyby tak zrobiły kolekcje składowe, ale bez takich metod nie byłoby to możliwe.
supercat

6
@ supercat: Nie mam pojęcia, o czym mówisz.
Robert Harvey

48

Prawidłowa odpowiedź znajduje się w dokumentacji Java , która stwierdza:

[d] metody efault umożliwiają dodawanie nowych funkcji do interfejsów bibliotek i zapewniają zgodność binarną z kodem napisanym dla starszych wersji tych interfejsów.

Jest to od dawna źródło bólu w Javie, ponieważ interfejsy były w stanie ewoluować po upublicznieniu. (Treść dokumentacji jest związana z artykułem, do którego odsyłacie w komentarzu: Ewolucja interfejsu za pomocą wirtualnych metod rozszerzenia .) Ponadto szybkie przyjęcie nowych funkcji (np. Lambdas i nowych interfejsów API strumienia) można osiągnąć tylko poprzez rozszerzenie istniejące interfejsy kolekcji i zapewniające domyślne implementacje. Zerwanie zgodności binarnej lub wprowadzenie nowych interfejsów API oznaczałoby, że minie kilka lat, zanim najważniejsze funkcje Java 8 będą w powszechnym użyciu.

Powód dopuszczenia metod statycznych w interfejsach jest ponownie ujawniony w dokumentacji: [t] on ułatwia organizowanie metod pomocniczych w bibliotekach; możesz zachować metody statyczne specyficzne dla interfejsu w tym samym interfejsie, a nie w osobnej klasie. Innymi słowy, takie statyczne klasy użyteczności, jak java.util.Collectionsteraz, można (ostatecznie) uznać za anty-wzorzec, ogólnie (oczywiście nie zawsze ). Domyślam się, że dodanie obsługi tego zachowania było trywialne po wdrożeniu wirtualnych metod rozszerzenia, w przeciwnym razie prawdopodobnie nie zostałoby to zrobione.

W podobnym tonie, przykładem na to, jak te nowe funkcje mogą być korzystne jest, aby rozważyć jedną klasę, która niedawno mnie irytowało, java.util.UUID. W rzeczywistości nie zapewnia wsparcia dla typów UUID 1, 2 lub 5 i nie można go łatwo modyfikować. Utknął także w predefiniowanym losowym generatorze, którego nie można zastąpić. Implementacja kodu dla nieobsługiwanych typów UUID wymaga albo bezpośredniej zależności od interfejsu API innej firmy niż interfejsu, albo utrzymania kodu konwersji i kosztu dodatkowego odśmiecania. Metodami statycznymi UUIDmożna zamiast tego zdefiniować interfejs, umożliwiając rzeczywiste implementacje brakujących elementów. (Gdyby UUIDzostały pierwotnie zdefiniowane jako interfejs, prawdopodobnie mielibyśmy coś niezręcznegoUuidUtil klasa z metodami statycznymi, co też byłoby okropne.) Wiele podstawowych interfejsów API Javy ulega degradacji, ponieważ nie opierają się na interfejsach, ale od wersji 8 liczba wymówek tego złego zachowania na szczęście zmniejszyła się.

Nie jest poprawne stwierdzenie, że [t] nie ma praktycznie żadnej różnicy między interfejsem a klasą abstrakcyjną , ponieważ klasy abstrakcyjne mogą mieć stan (to znaczy deklarować pola), podczas gdy interfejsy nie. Nie jest zatem równoważny wielokrotnemu dziedziczeniu ani nawet dziedziczeniu w stylu mixin. Właściwe miksy (takie jak cechy Groovy 2.3 ) mają dostęp do stanu. (Groovy obsługuje również metody rozszerzania statycznego).

Moim zdaniem nie jest dobrym pomysłem podążać za przykładem Dovala . Interfejs ma definiować kontrakt, ale nie powinien go egzekwować. (W każdym razie nie w Javie.) Za prawidłową weryfikację implementacji odpowiada zestaw testów lub inne narzędzie. Definiowanie umów można wykonać za pomocą adnotacji, a OVal jest dobrym przykładem, ale nie wiem, czy obsługuje ograniczenia zdefiniowane w interfejsach. Taki system jest wykonalny, nawet jeśli obecnie nie istnieje. (Strategie obejmują dostosowanie czasu kompilacji javacza pomocą procesora adnotacjiAPI i generowanie kodu bajtowego w czasie wykonywania). Najlepiej byłoby, gdyby kontrakty były egzekwowane w czasie kompilacji, aw najgorszym przypadku przy użyciu zestawu testów, ale rozumiem, że nie ma sensu egzekwować środowiska wykonawczego. Kolejnym interesującym narzędziem, które może pomóc w programowaniu kontraktów w Javie, jest Checker Framework .


1
W celu dalszego śledzenia mojego ostatniego akapitu (tj. Nie wymuszaj umów w interfejsach ), warto zauważyć, że defaultmetody nie mogą zastąpić equals, hashCodei toString. Bardzo pouczającą analizę kosztów i korzyści tego, dlaczego jest to niedozwolone, można znaleźć tutaj: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/...
ngreen

Szkoda, że ​​Java ma tylko jedną wirtualną equalsi jedną hashCodemetodę, ponieważ istnieją dwa różne rodzaje równości, które kolekcje mogą potrzebować do przetestowania, a elementy, które implementowałyby wiele interfejsów, mogą utknąć w sprzecznych wymaganiach umownych. Możliwość korzystania z list, które nie ulegną zmianie jako hashMapklucze, jest pomocna, ale są też chwile, kiedy pomocne byłoby przechowywanie kolekcji w hashMapdopasowanych rzeczach opartych na równoważności, a nie na obecnym stanie [równoważność oznacza stan dopasowania i niezmienność ] .
supercat

Cóż, Java ma obejście z komparatorami i interfejsami porównywalnymi. Ale myślę, że są trochę brzydkie.
ngreen

Interfejsy te są obsługiwane tylko w przypadku niektórych typów kolekcji i stwarzają własny problem: komparator może sam potencjalnie obudować stan (np. Specjalistyczny komparator ciągu może zignorować konfigurowalną liczbę znaków na początku każdego łańcucha, w którym to przypadku liczba znaki, które należy zignorować, byłyby częścią stanu komparatora), który z kolei stałby się częścią stanu każdej kolekcji, która została przez niego posortowana, ale nie ma zdefiniowanego mechanizmu pytającego dwóch komparatorów, czy są one równoważne.
supercat

Och tak, odczuwam ból komparatorów. Pracuję nad strukturą drzewa, która powinna być prosta, ale nie dlatego, że tak trudno jest uzyskać prawidłowy komparator. Prawdopodobnie zamierzam napisać niestandardową klasę drzewa, aby problem zniknął.
ngreen

44

Ponieważ możesz odziedziczyć tylko jedną klasę. Jeśli masz dwa interfejsy, których implementacje są na tyle złożone, że potrzebujesz abstrakcyjnej klasy bazowej, te dwa interfejsy wykluczają się w praktyce.

Alternatywą jest konwersja tych abstrakcyjnych klas bazowych na zbiór metod statycznych i zamiana wszystkich pól w argumenty. Pozwoliłoby to każdemu implementatorowi interfejsu wywoływać metody statyczne i uzyskiwać funkcjonalność, ale jest to strasznie dużo bojlerów w języku, który jest już zbyt gadatliwy.


Jako motywujący przykład tego, dlaczego udostępnianie implementacji w interfejsach może być przydatne, rozważ ten interfejs stosu:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Nie ma sposobu, aby zagwarantować, że gdy ktoś zaimplementuje interfejs, popwyrzuci wyjątek, jeśli stos jest pusty. Możemy wymusić tę regułę, dzieląc ją popna dwie metody: public finalmetodę, która wymusza kontrakt i protected abstractmetodę, która wykonuje faktyczne popping.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Nie tylko zapewniamy, że wszystkie wdrożenia są zgodne z umową, ale także uwolniliśmy je od konieczności sprawdzania, czy stos jest pusty i zgłaszania wyjątku. To wielka wygrana! ... z wyjątkiem tego, że musieliśmy zmienić interfejs na klasę abstrakcyjną. W języku z jednym dziedzictwem oznacza to dużą utratę elastyczności. To sprawia, że ​​twoje przyszłe interfejsy wykluczają się wzajemnie. Możliwość implementacji, które polegają wyłącznie na samych metodach interfejsu, rozwiązałaby problem.

Nie jestem pewien, czy podejście Java 8 do dodawania metod do interfejsów pozwala na dodawanie metod końcowych czy chronionych metod abstrakcyjnych, ale wiem, że język D na to pozwala i zapewnia natywną obsługę projektowania według umowy . Nie ma niebezpieczeństwa w tej technice, ponieważ popjest ona ostateczna, więc żadna klasa implementująca nie może jej zastąpić.

Jeśli chodzi o domyślne implementacje metod nadpisywalnych, zakładam, że wszelkie domyślne implementacje dodane do interfejsów API Java opierają się tylko na umowie interfejsu, do którego zostały dodane, więc każda klasa, która poprawnie implementuje interfejs, będzie również działać poprawnie z domyślnymi implementacjami.

Ponadto,

Praktycznie nie ma różnicy między interfejsem a klasą abstrakcyjną (inną niż wielokrotne dziedziczenie). W rzeczywistości możesz emulować zwykłą klasę za pomocą interfejsu.

Nie jest to do końca prawdą, ponieważ nie można deklarować pól w interfejsie. Żadna metoda napisana w interfejsie nie może polegać na żadnych szczegółach implementacji.


Jako przykład na korzyść metod statycznych w interfejsach, rozważ klasy narzędzi, takie jak Kolekcje w Java API. Ta klasa istnieje tylko dlatego, że tych metod statycznych nie można zadeklarować w odpowiednich interfejsach. Collections.unmodifiableListrównie dobrze mógł zostać zadeklarowany w Listinterfejsie i łatwiej byłoby go znaleźć.


4
Kontrargument: ponieważ metody statyczne, jeśli są poprawnie zapisane, są niezależne, mają większy sens w osobnej klasie statycznej, w której można je gromadzić i kategoryzować według nazwy klasy, a mniej sensowne w interfejsie, gdzie są one zasadniczo wygodą który zachęca do nadużyć, takich jak utrzymywanie stanu statycznego w obiekcie lub wywoływanie efektów ubocznych, co powoduje, że metody statyczne są niestabilne.
Robert Harvey

3
@RobertHarvey Co powstrzyma cię przed robieniem równie głupich rzeczy, jeśli twoja metoda statyczna jest w klasie? Ponadto metoda w interfejsie może w ogóle nie wymagać żadnego stanu. Być może po prostu próbujesz wyegzekwować umowę. Załóżmy, że masz Stackinterfejs i chcesz się upewnić, że gdy popzostanie wywołany z pustym stosem, zostanie zgłoszony wyjątek. Biorąc pod uwagę abstrakcyjne metody boolean isEmpty()i protected T pop_impl(), można wdrożyć final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }To wymusza kontrakt na WSZYSTKICH implementatorach.
Doval

Czekaj, co? Metody push i pop na stosie nie będą static.
Robert Harvey

@RobertHarvey Byłbym bardziej zrozumiały, gdyby nie limit znaków w komentarzach, ale opowiadałem się za domyślnymi implementacjami w interfejsie, a nie metodami statycznymi.
Doval

8
Myślę, że domyślne metody interfejsu to raczej hack, który został wprowadzony, aby móc rozszerzyć standardową bibliotekę bez potrzeby dostosowywania istniejącego kodu na jej podstawie.
Giorgio

2

Być może celem było zapewnienie możliwości tworzenia klas mixin poprzez zastąpienie potrzeby wstrzykiwania statycznych informacji lub funkcjonalności za pomocą zależności.

Ten pomysł wydaje się związany z tym, jak można użyć metod rozszerzenia w języku C #, aby dodać zaimplementowaną funkcjonalność do interfejsów.


1
Metody rozszerzeń nie dodają funkcjonalności interfejsów. Metody rozszerzeń to po prostu cukier składniowy do wywoływania metod statycznych w klasie przy użyciu wygodnej list.sort(ordering);formy.
Robert Harvey

Jeśli spojrzysz na IEnumerableinterfejs w C #, możesz zobaczyć, w jaki sposób implementacja metod rozszerzenia do tego interfejsu (podobnie jak LINQ to Objectsrobi) dodaje funkcjonalność dla każdej implementowanej klasy IEnumerable. Właśnie to miałem na myśli, dodając funkcjonalność.
rae1

2
To wielka zaleta metod rozszerzeń; dają złudzenie, że wykorzystujesz funkcjonalność do klasy lub interfejsu. Po prostu nie myl tego z dodawaniem rzeczywistych metod do klasy; metody klasy mają dostęp do prywatnych elementów obiektu, metody rozszerzające nie (ponieważ są tak naprawdę tylko innym sposobem wywoływania metod statycznych).
Robert Harvey

2
Właśnie i dlatego widzę pewien związek z posiadaniem statycznych lub domyślnych metod w interfejsie w Javie; implementacja opiera się na tym, co jest dostępne dla interfejsu, a nie na samej klasie.
rae1

1

Dwa główne cele, które widzę w defaultmetodach (niektóre przypadki użycia służą obu celom):

  1. Cukier składniowy. Klasa użyteczności może służyć temu celowi, ale metody instancji są ładniejsze.
  2. Rozszerzenie istniejącego interfejsu. Wdrożenie jest ogólne, ale czasami nieefektywne.

Gdyby chodziło tylko o drugi cel, nie zobaczyłbyś tego w zupełnie nowym interfejsie, takim jak Predicate. Wszystkie @FunctionalInterfaceinterfejsy z adnotacjami muszą mieć dokładnie jedną abstrakcyjną metodę, aby lambda mogła ją zaimplementować. Dodano defaultpodobne metody and, or, negateto tylko narzędzie, a nie mają ich zastąpić. Jednak czasem metody statyczne byłyby lepsze .

Jeśli chodzi o rozszerzenie istniejących interfejsów - nawet tam, niektóre nowe metody to po prostu cukier składniowy. Metody Collectionjak stream, forEach, removeIf- w zasadzie, to tylko narzędzie, nie trzeba zastąpić. A potem są takie metody spliterator. Domyślna implementacja jest nieoptymalna, ale hej, przynajmniej kod się kompiluje. Korzystaj z tego tylko wtedy, gdy interfejs jest już opublikowany i powszechnie używany.


Jeśli chodzi o staticmetody, myślę, że inni dobrze to opisują: pozwala to interfejsowi być jego własną klasą użyteczności. Może moglibyśmy się pozbyć Collectionsw przyszłości Javy? Set.empty()kołysałby się.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.