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:
- metody statyczne nie należą do interfejsów. Należą do klas użytkowych.
- „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 interfejsuIx
, wówczas klasaCx
implementującaIx
i posiadająca prywatnąfoo
metodę 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ą?
java
programming-languages
interfaces
java8
Pan Smith
źródło
źródło
@Deprecated
kategorii! 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!Odpowiedzi:
Dobrym motywującym przykładem domyślnych metod jest standardowa biblioteka Java, w której jest teraz
zamiast
Nie sądzę, aby mogli zrobić to inaczej bez więcej niż jednej identycznej implementacji
List.sort
.źródło
IEnumerable<Byte>.Append
aby do nich dołączyć, a następnie zadzwońCount
, a następnie powiedz mi, jak metody rozszerzenia rozwiązują problem. GdybyCountIsKnown
iCount
byli członkamiIEnumerable<T>
, powrót zAppend
mógłby reklamować,CountIsKnown
gdyby tak zrobiły kolekcje składowe, ale bez takich metod nie byłoby to możliwe.Prawidłowa odpowiedź znajduje się w dokumentacji Java , która stwierdza:
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.Collections
teraz, 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 statycznymiUUID
można zamiast tego zdefiniować interfejs, umożliwiając rzeczywiste implementacje brakujących elementów. (GdybyUUID
został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
javac
za 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 .źródło
default
metody nie mogą zastąpićequals
,hashCode
itoString
. 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/...equals
i jednąhashCode
metodę, 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 jakohashMap
klucze, jest pomocna, ale są też chwile, kiedy pomocne byłoby przechowywanie kolekcji whashMap
dopasowanych rzeczach opartych na równoważności, a nie na obecnym stanie [równoważność oznacza stan dopasowania i niezmienność ] .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:
Nie ma sposobu, aby zagwarantować, że gdy ktoś zaimplementuje interfejs,
pop
wyrzuci wyjątek, jeśli stos jest pusty. Możemy wymusić tę regułę, dzieląc jąpop
na dwie metody:public final
metodę, która wymusza kontrakt iprotected abstract
metodę, która wykonuje faktyczne popping.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ż
pop
jest 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,
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.unmodifiableList
równie dobrze mógł zostać zadeklarowany wList
interfejsie i łatwiej byłoby go znaleźć.źródło
Stack
interfejs i chcesz się upewnić, że gdypop
zostanie wywołany z pustym stosem, zostanie zgłoszony wyjątek. Biorąc pod uwagę abstrakcyjne metodyboolean isEmpty()
iprotected T pop_impl()
, można wdrożyćfinal T pop() { isEmpty()) throw PopException(); else return pop_impl(); }
To wymusza kontrakt na WSZYSTKICH implementatorach.static
.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.
źródło
list.sort(ordering);
formy.IEnumerable
interfejs w C #, możesz zobaczyć, w jaki sposób implementacja metod rozszerzenia do tego interfejsu (podobnie jakLINQ to Objects
robi) dodaje funkcjonalność dla każdej implementowanej klasyIEnumerable
. Właśnie to miałem na myśli, dodając funkcjonalność.Dwa główne cele, które widzę w
default
metodach (niektóre przypadki użycia służą obu celom):Gdyby chodziło tylko o drugi cel, nie zobaczyłbyś tego w zupełnie nowym interfejsie, takim jak
Predicate
. Wszystkie@FunctionalInterface
interfejsy z adnotacjami muszą mieć dokładnie jedną abstrakcyjną metodę, aby lambda mogła ją zaimplementować. Dodanodefault
podobne metodyand
,or
,negate
to 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
Collection
jakstream
,forEach
,removeIf
- w zasadzie, to tylko narzędzie, nie trzeba zastąpić. A potem są takie metodyspliterator
. 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
static
metody, myślę, że inni dobrze to opisują: pozwala to interfejsowi być jego własną klasą użyteczności. Może moglibyśmy się pozbyćCollections
w przyszłości Javy?Set.empty()
kołysałby się.źródło