Dlaczego kolekcje Java zostały zaimplementowane za pomocą „opcjonalnych metod” w interfejsie?

69

Podczas mojej pierwszej implementacji rozszerzającej środowisko kolekcji Java byłem zaskoczony, widząc, że interfejs kolekcji zawiera metody zadeklarowane jako opcjonalne. Oczekuje się, że implementator zgłosi UnsupportedOperationExceptions, jeśli nie jest obsługiwany. Od razu uderzyło mnie to jako kiepski wybór interfejsu API.

Po przeczytaniu dużej części doskonałej książki Joshuy Blocha „Skuteczna Java”, a później nauczeniu się, że może być odpowiedzialny za te decyzje, nie wydawało się, że żeluje to z zasadami zawartymi w książce. Pomyślałem, że zadeklarowanie dwóch interfejsów: Collection i MutableCollection, które rozszerzy Collection przy pomocy „opcjonalnych” metod, doprowadziłoby do znacznie łatwiejszego do utrzymania kodu klienta.

Jest to doskonałe podsumowanie kwestii tutaj .

Czy był dobry powód, dla którego wybrano opcjonalne metody zamiast implementacji dwóch interfejsów?

glenviewjeff
źródło
IMO nie ma dobrego powodu. C ++ STL został stworzony przez Stepanova na początku lat 80-tych. Mimo że jego użyteczność była ograniczona przez niezręczną składnię szablonów C ++, jest to wzór spójności i użyteczności w porównaniu z klasami kolekcji Java.
kevin cline
Wystąpiło „przenoszenie” C ++ STL na Javę. Ale był 5 razy większy w klasie. Mając to na uwadze, nie kupuję tego, że większy jest bardziej spójny i użyteczny. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman
@ m3th0dman Jest znacznie większy w Javie, ponieważ Java nie ma nic równoważnego w sile szablonów C ++.
kevin cline
Może to tylko jakiś dziwny styl wyrobiłem, ale mój kod ma tendencję do traktowania wszystkich zbiorów jako „tylko do odczytu” (dokładniej, po prostu coś, co można liczyć albo nad którym iteracyjne) z wyjątkiem dla kilku metod, które faktycznie stworzyć kolekcje. Prawdopodobnie dobra praktyka i tak (szczególnie w przypadku współbieżności). A opcjonalne metody, na które tak wielu narzeka, nigdy nie były dla mnie prawdziwym problemem. Również koszmary generyczne z „super” i „przedłuża” nigdy nie były (dużym) problemem. Zastanawiam się, czy inni używają tej ogólnej praktyki?
user949300

Odpowiedzi:

28

FAQ dostarcza odpowiedzi. Krótko mówiąc, widzieli potencjalną eksplozję kombinatoryczną potrzebnych interfejsów z modyfikowalnym, niemodyfikowalnym widokiem, tylko do usuwania, tylko do dodawania, o stałej długości, niezmienny (do wątkowania) i tak dalej dla każdego możliwego zestawu zaimplementowanych metod opcji.

maniak zapadkowy
źródło
6
Można by tego wszystkiego uniknąć, gdyby Java miała constsłowo kluczowe takie jak C ++.
Etienne de Martel
@Etienne: Lepsze byłyby metaklasy takie jak Python. Następnie można programowo zbudować kombinatoryczną liczbę interfejsów. Problem z const jest to, że daje tylko jeden lub dwa wymiary: vector<int>, const vector<int>, vector<const int>, const vector<const int>. Jak dotąd dobrze, ale próbujesz zaimplementować grafy i chcesz, aby struktura wykresu była stała, ale atrybuty węzła można modyfikować itp.
Neil G
2
@Etienne, kolejny powód, aby podnieść listę „Learn Scala” na mojej liście rzeczy do zrobienia!
glenviewjeff
2
Nie rozumiem tylko, dlaczego nie stworzyli canmetody, która sprawdziłaby, czy operacja jest możliwa? Dzięki temu interfejs byłby prosty i szybki.
Mehrdad
3
@Etienne de Martel Nonsense. Jak mogłoby to pomóc w wybuchu kombinatorycznym?
Tom Hawtin - tackline
10

Wydaje mi się, że Interface Segregation Principlewtedy nie było tak dobrze zbadane, jak teraz; ten sposób robienia rzeczy (tj. twój interfejs zawiera wszystkie możliwe operacje i masz metody „zdegenerowane”, które generują wyjątki dla tych, których nie potrzebujesz) był popularny zanim SOLID i ISP stały się de facto standardem dla kodu jakości.

Wayne Molina
źródło
2
Dlaczego głosowanie negatywne? Ktoś nie lubi ISP?
Wayne Molina
Warto również zauważyć, że w ramach, które obsługują wariancję, OGROMNA korzyść polega na oddzieleniu tych aspektów interfejsu, które mogą być kowariantne lub sprzeczne z tymi, które są zasadniczo niezmienne. Nawet przy braku takiego wsparcia, w ramach, które nie używają wymazywania typu, segregacja aspektów interfejsu, które są niezależne od typu, miałaby wartość (np. Pozwalając uzyskać Countkolekcję bez martwienia się o to, jakie typy elementów zawiera), ale w ramach opartych na usuwaniu czcionek, takich jak Java, nie jest to taki problem.
supercat
4

Chociaż niektórzy ludzie nie znoszą „metod opcjonalnych”, w wielu przypadkach mogą oferować lepszą semantykę niż wysoce rozdzielone interfejsy. Pozwalają między innymi na to, że obiekt może zyskać zdolności lub cechy w trakcie swojego życia, lub że obiekt (zwłaszcza obiekt otoki) może nie wiedzieć, kiedy jest skonstruowany, jakie dokładnie umiejętności powinien zgłosić.

Chociaż raczej nie będę nazywał klas języka Java wzorami dobrego projektowania, sugerowałbym, że dobry framework kolekcji powinien u podstaw zawierać dużą liczbę opcjonalnych metod wraz ze sposobami zapytania kolekcji o jej cechy i możliwości . Taki projekt pozwoli na użycie pojedynczej klasy opakowania z dużą różnorodnością kolekcji bez przypadkowego zaciemnienia umiejętności, jakie może mieć kolekcja podstawowa. Jeśli metody nie były opcjonalne, konieczne byłoby posiadanie innej klasy opakowania dla każdej kombinacji funkcji obsługiwanych przez kolekcje, w przeciwnym razie niektóre opakowania nie nadawałyby się do użytku w niektórych sytuacjach.

Na przykład, jeśli kolekcja obsługuje pisanie elementu według indeksu lub dodawanie elementów na końcu, ale nie obsługuje wstawiania elementów na środku, to kod chcący obudować go w opakowaniu, który rejestrowałby wszystkie wykonane na nim akcje, potrzebowałby wersji opakowania, które zapewniało dokładną kombinację obsługiwanych zdolności, lub jeśli żadna nie była dostępna, musiałaby użyć opakowania, które obsługuje dopisywanie lub zapisywanie według indeksu, ale nie oba. Jeśli jednak ujednolicony interfejs kolekcji zapewnia wszystkie trzy metody jako „opcjonalne”, ale następnie obejmuje metody wskazujące, która z opcjonalnych metod będzie użyteczna, wówczas pojedyncza klasa opakowania może obsługiwać kolekcje, które implementują dowolną kombinację funkcji. Na pytanie, jakie funkcje obsługuje, opakowanie może po prostu zgłosić wszystko, co obsługuje kolekcja enkapsulowana.

Zauważ, że istnienie „opcjonalnych zdolności” może w niektórych przypadkach pozwolić zbiorczym kolekcjom na wdrożenie niektórych funkcji w sposób, który byłby znacznie bardziej wydajny niż byłoby to możliwe, gdyby umiejętności były zdefiniowane przez istnienie implementacji. Załóżmy na przykład, że concatenateużyto metody do utworzenia zbioru złożonego z dwóch innych, z których pierwszym był ArrayList z 1 000 000 elementów, a ostatnim z nich była kolekcja dwudziestoelementowa, którą można było iterować tylko od początku. Jeśli poproszono by o zbiór złożony o 1 000 013 element (indeks 1 000 012), mógłby zapytać ArrayList, ile elementów zawiera (tj. 1 000 000), odjąć to od żądanego indeksu (dając 12), odczytać i pominąć dwanaście elementów od drugiego kolekcja, a następnie zwróć następny element.

W takiej sytuacji, mimo że kolekcja złożona nie miałaby natychmiastowego sposobu na zwrócenie pozycji według indeksu, zapytanie kolekcji zbiorczej o 1 000 013 pozycji byłoby nadal znacznie szybsze niż odczytanie z niej 1 000 013 pozycji z osobna i zignorowanie wszystkich oprócz ostatniej jeden.

supercat
źródło
@kevincline: Czy nie zgadzasz się z cytowanym sformułowaniem? Rozważę zaprojektowanie środków, za pomocą których implementacje interfejsu mogą opisywać ich podstawowe cechy i umiejętności, jako jeden z najważniejszych aspektów projektowania interfejsu, nawet jeśli nie zwraca się na niego dużej uwagi. Jeśli różne implementacje interfejsu będą miały różne optymalne sposoby wykonywania niektórych typowych zadań, klienci powinni dbać o wydajność, aby wybrać najlepsze podejście w scenariuszach, w których ma to znaczenie.
supercat
1
przepraszam, niekompletny komentarz. Już miałem powiedzieć, że „„ sposoby zadawania pytań o kolekcję ”…” przenoszą to, co powinno być sprawdzaniem czasu kompilacji do czasu wykonywania.
kevin cline
@kevincline: W przypadkach, gdy klient musi mieć określoną zdolność i nie może bez niej żyć, pomocne mogą być kontrole czasu kompilacji. Z drugiej strony istnieje wiele sytuacji, w których kolekcje mogą mieć zdolności wykraczające poza te, które można zagwarantować w czasie kompilacji. W niektórych przypadkach sensowne może być posiadanie interfejsu pochodnego, którego umowa określa, że ​​wszystkie uzasadnione implementacje interfejsu pochodnego muszą obsługiwać określone metody, które są opcjonalne w interfejsie podstawowym, ale w przypadkach, w których kod będzie w stanie obsłużyć coś, niezależnie od tego, czy ma jakąś funkcję ...
supercat
... ale skorzysta z tego, że istnieje, lepiej, żeby kod zapytał obiekt, czy obsługuje tę funkcję, niż zapytał system typów, czy typ obiektu obiecuje wspierać tę funkcję. Jeśli konkretny „konkretny” interfejs będzie szeroko stosowany, można uwzględnić AsXXXmetodę w interfejsie podstawowym, która zwróci obiekt, na który jest wywoływany, jeśli implementuje ten interfejs, zwróci obiekt opakowania, który obsługuje ten interfejs, jeśli to możliwe, lub wyrzuci wyjątek, jeśli nie. Na przykład ImmutableCollectioninterfejs może wymagać umowy ...
supercat
2
Wystąpił problem z twoją propozycją: wzorzec „pytaj, a potem zrób” ma problem z współbieżnością, jeśli możliwości obiektu mogą ulec zmianie w trakcie jego życia. (Jeśli i tak będziesz musiał zaryzykować wyjątek, równie dobrze możesz nie zawracać sobie głowy pytaniem ...)
jhominal 24.04.15
-1

Przypisałbym to oryginalnym programistom, którzy nie wiedzieli wtedy lepiej. Przeszliśmy długą drogę w projektowaniu OO od 1998 roku, kiedy po raz pierwszy wydano Java 2 i kolekcje. To, co wydaje się oczywistym złym projektem, nie było już tak oczywiste we wczesnych latach OOP.

Ale mogło to zostać zrobione, aby zapobiec dodatkowemu rzucaniu. Gdyby to był drugi interfejs, musiałbyś rzutować instancje kolekcji, aby wywoływać te opcjonalne metody, co również jest trochę brzydkie. W tej chwili możesz natychmiast złapać UnsupportedOperationException i naprawić swój kod. Ale gdyby istniały dwa interfejsy, trzeba by użyć instancji i rzutowania w dowolnym miejscu. Być może uznali to za ważny kompromis. Również we wczesnej wersji Java 2-dniowe wystąpienie instof było bardzo niezadowolone z powodu jego niskiej wydajności, mogli próbować zapobiec jego nadmiernemu użyciu.

Oczywiście to wszystko dzikie spekulacje. Wątpię, czy moglibyśmy odpowiedzieć na to pytanie na pewno, chyba że zadzwoni jedna z oryginalnych kolekcji architektów.

Jberg
źródło
7
Jaki casting? Jeśli metoda zwraca a, Collectiona nie a MutableCollection, jest to wyraźny znak, że nie jest przeznaczona do modyfikacji. Nie wiem, dlaczego ktoś musiałby je rzucić. Posiadanie osobnych interfejsów oznacza, że ​​będziesz otrzymywać tego rodzaju błędy w czasie kompilacji zamiast uzyskiwania wyjątku w czasie wykonywania. Im wcześniej pojawi się błąd, tym lepiej.
Etienne de Martel
1
Ponieważ jest to o wiele mniej elastyczne, jedną z największych zalet biblioteki kolekcji jest to, że możesz zwrócić interfejsy wysokiego poziomu wszędzie i nie martwić się o faktyczną implementację. Jeśli używasz dwóch interfejsów, jesteś teraz ściślej powiązany. W większości przypadków po prostu chcesz zwrócić List, a nie ImmutableList, ponieważ zwykle chcesz zostawić to klasie wywołującej w celu ustalenia.
Jberg
6
Jeśli dam wam kolekcję tylko do odczytu, to dlatego, że nie chcę, aby była modyfikowana. Zgłaszanie wyjątku i poleganie na dokumentacji jest jak łatka. W C ++ po prostu zwróciłbym constobiekt, natychmiast mówiąc użytkownikowi, że obiektu nie można modyfikować.
Etienne de Martel
7
1998 nie był „początkiem projektowania OO”.
quant_dev