Biorąc pod uwagę następujący przykład (użycie JUnit z dopasowaniami Hamcrest):
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));
Nie kompiluje się z assertThat
sygnaturą metody JUnit :
public static <T> void assertThat(T actual, Matcher<T> matcher)
Komunikat o błędzie kompilatora to:
Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
<? extends java.io.Serializable>>>)
Jeśli jednak zmienię assertThat
podpis metody na:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Następnie kompilacja działa.
Więc trzy pytania:
- Dlaczego dokładnie nie kompiluje się bieżąca wersja? Chociaż niejasno rozumiem tu kwestie kowariancji, z pewnością nie mógłbym tego wyjaśnić, gdybym musiał.
- Czy zmiana
assertThat
metody na ma jakiś minusMatcher<? extends T>
? Czy są inne przypadki, które by się zepsuły, gdybyś to zrobił? - Czy jest jakiś sens uogólnienia
assertThat
metody w JUnit?Matcher
Klasa nie wydaje się potrzebne, ponieważ JUnit wywołuje metodę zapałek, który nie jest wpisany na wszelkie generycznych, a po prostu wygląda jak próba wymuszenia typu bezpieczeństwa, który nic nie robi, boMatcher
po prostu nie będzie w rzeczywistości dopasuj, a test się nie powiedzie. Brak niebezpiecznych operacji (a przynajmniej tak się wydaje).
Dla odniesienia, oto implementacja JUnit assertThat
:
public static <T> void assertThat(T actual, Matcher<T> matcher) {
assertThat("", actual, matcher);
}
public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
if (!matcher.matches(actual)) {
Description description = new StringDescription();
description.appendText(reason);
description.appendText("\nExpected: ");
matcher.describeTo(description);
description
.appendText("\n got: ")
.appendValue(actual)
.appendText("\n");
throw new java.lang.AssertionError(description.toString());
}
}
Odpowiedzi:
Po pierwsze - muszę cię przekierować na http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - wykonuje niesamowitą robotę.
Podstawową ideą jest to, że używasz
kiedy faktycznym parametrem może być
SomeClass
dowolny jego podtyp.W twoim przykładzie
Mówisz, że
expected
mogą zawierać obiekty klasy reprezentujące dowolną klasę, która implementujeSerializable
. Twoja mapa wyników mówi, że może przechowywać tylkoDate
obiekty klasy.Kiedy przechodzą w wyniku tego, że masz ustawione
T
dokładnieMap
odString
doDate
klasy obiektów, które nie odpowiadająMap
stanowiString
na cokolwiek to jestSerializable
.Jedną rzecz do sprawdzenia - czy na pewno chcesz,
Class<Date>
a nieDate
? MapęString
abyClass<Date>
nie brzmi strasznie użyteczne w ogóle (wszystko może pomieścić jestDate.class
jako wartości zamiast wystąpieńDate
)Jeśli chodzi o uogólnianie
assertThat
, chodzi o to, że metoda może zapewnićMatcher
przekazanie wartości pasującej do typu wyniku.źródło
Dzięki wszystkim, którzy odpowiedzieli na pytanie, naprawdę pomogło mi to wyjaśnić. Ostatecznie odpowiedź Scotta Stanchfielda zbliżyła się do tego, w jaki sposób ostatecznie ją zrozumiałem, ale ponieważ nie zrozumiałem go, kiedy napisał ją po raz pierwszy, staram się odtworzyć problem, aby mieć nadzieję, że ktoś inny skorzysta.
Mam zamiar ponownie sformułować pytanie w odniesieniu do listy, ponieważ ma on tylko jeden ogólny parametr i ułatwi to zrozumienie.
Celem sparametryzowanej klasy (takiej jak List
<Date>
lub Map<K, V>
jak w przykładzie) jest wymuszenie downcastu i posiadanie przez kompilator gwarancji, że jest to bezpieczne (bez wyjątków w czasie wykonywania).Rozważ przypadek Listy. Istota mojego pytania brzmi: dlaczego metoda, która przyjmuje typ T i Listę, nie zaakceptuje Listy czegoś znajdującego się w dalszej części łańcucha dziedziczenia niż T. Rozważmy ten wymyślony przykład:
To się nie skompiluje, ponieważ parametr list jest listą dat, a nie listą ciągów. Generics nie byłby bardzo przydatny, gdyby się to skompilowało.
To samo dotyczy mapy.
<String, Class<? extends Serializable>>
To nie to samo co mapa<String, Class<java.util.Date>>
. Nie są one kowariantne, więc jeśli chciałbym pobrać wartość z mapy zawierającej klasy dat i umieścić ją na mapie zawierającej elementy, które można serializować, to dobrze, ale podpis metody, który mówi:Chce być w stanie wykonać oba:
i
W tym przypadku, mimo że metoda junit tak naprawdę nie dba o te rzeczy, podpis metody wymaga kowariancji, której nie otrzymuje, dlatego się nie kompiluje.
Na drugie pytanie
Miałoby to wadę, że naprawdę akceptuje wszystko, gdy T jest obiektem, co nie jest intencją API. Chodzi o to, aby statycznie upewnić się, że moduł dopasowujący pasuje do rzeczywistego obiektu, i nie ma sposobu, aby wykluczyć Obiekt z tego obliczenia.
Odpowiedź na trzecie pytanie brzmi: nic nie zostanie utracone, jeśli chodzi o niesprawdzoną funkcjonalność (nie byłoby niebezpiecznego rzutowania czcionek w interfejsie API JUnit, gdyby ta metoda nie była uogólniona), ale starają się osiągnąć coś innego - statycznie zapewnić, że prawdopodobnie dwa parametry będą do siebie pasować.
EDYCJA (po dalszej kontemplacji i doświadczeniu):
Jednym z dużych problemów z podpisem metody assertThat jest próba zrównania zmiennej T z ogólnym parametrem T. To nie działa, ponieważ nie są one kowariantne. Na przykład możesz mieć literę T, która jest równa,
List<String>
ale następnie przekazuje dopasowanie, na którym działa kompilatorMatcher<ArrayList<T>>
. Teraz, jeśli nie byłby to parametr typu, wszystko byłoby w porządku, ponieważ List i ArrayList są kowariantne, ale ponieważ Generics, jeśli chodzi o kompilator, wymagają ArrayList, nie może tolerować listy z powodów, które, mam nadzieję, są jasne z góry.źródło
List<Date>
z metody typuList<Object>
? To powinno być bezpieczne, nawet jeśli nie zezwala na to Java.Sprowadza się do:
Widać, że odwołanie do klasy c1 może zawierać długą instancję (ponieważ w danym momencie mógł istnieć obiekt leżący u podstaw
List<Long>
), ale oczywiście nie można rzutować na datę, ponieważ nie ma gwarancji, że „nieznana” klasa to data. Nie jest bezpieczny, więc kompilator go nie zezwala.Jeśli jednak wprowadzimy jakiś inny obiekt, powiedzmy List (w twoim przykładzie jest to Matcher), wówczas spełnią się następujące warunki:
... Jeśli jednak typ listy zostanie zmieniony? rozszerza T zamiast T ....
Myślę, że zmieniając
Matcher<T> to Matcher<? extends T>
, w zasadzie wprowadzasz scenariusz podobny do przypisywania l1 = l2;Zagnieżdżanie symboli wieloznacznych jest nadal bardzo mylące, ale mam nadzieję, że ma to sens, dlaczego pomaga zrozumieć generyczne, patrząc na to, jak można przypisywać sobie nawzajem ogólne. Jest to również mylące, ponieważ kompilator wnioskuje o typie T podczas wykonywania wywołania funkcji (nie mówisz wprost, że to T).
źródło
Powodem oryginalny kod nie kompilacji jest to, że
<? extends Serializable>
robi nie znaczy, „każda klasa, która rozszerza SERIALIZABLE”, ale „jakiś nieznany, ale specyficzna klasa, która rozszerza SERIALIZABLE”.Na przykład, biorąc pod uwagę kod, jak napisane jest w pełni uzasadniona, aby przypisać
new TreeMap<String, Long.class>()>
doexpected
. Gdyby kompilator zezwolił na kompilację kodu,assertThat()
przypuszczalnie zostałby uszkodzony, ponieważ oczekiwałbyDate
obiektów zamiastLong
obiektów znalezionych na mapie.źródło
<Serializable>
Jednym ze sposobów, w jaki rozumiem symbole wieloznaczne, jest myślenie, że symbol wieloznaczny nie określa rodzaju możliwych obiektów, które może zawierać odnośnik ogólny, ale typ innych odnośników ogólnych, z którymi jest zgodny (może to brzmieć myląco ...) Jako taka, pierwsza odpowiedź jest bardzo myląca w swoim brzmieniu.
Innymi słowy,
List<? extends Serializable>
oznacza, że możesz przypisać to odwołanie do innych list, na których typem jest nieznany typ lub podklasa Serializable. NIE myśl o tym w kategoriach POJEDYNCZEJ LISTY zdolnej do przechowywania podklas Serializowalnego (ponieważ jest to niepoprawna semantyka i prowadzi do nieporozumienia Ogólnych).źródło
<? extends T>
kompiluje się metodę z Matcherem ?Wiem, że to stare pytanie, ale chciałbym podzielić się przykładem, który moim zdaniem dość dobrze wyjaśnia ograniczone symbole wieloznaczne.
java.util.Collections
oferuje tę metodę:Jeśli mamy listę
T
, lista może oczywiście zawierać instancje typów, które się rozszerzająT
. Jeśli lista zawiera zwierzęta, lista może zawierać zarówno psy, jak i koty (oba zwierzęta). Psy mają właściwość „woofVolume”, a koty mają właściwość „meowVolume”. Chociaż możemy chcieć sortować według tych właściwości, szczególnie dla podklasT
, jak możemy oczekiwać, że ta metoda to zrobi? Ograniczeniem komparatora jest to, że może on porównywać tylko dwie rzeczy tylko jednego typu (T
). Tak więc, wymaganie po prostuComparator<T>
uczyni tę metodę użyteczną. Ale twórca tej metody uznał, że jeśli coś jestT
, to jest to również instancja nadklasyT
. Dlatego pozwala nam korzystać z KomparatoraT
lub dowolnej nadklasyT
, tj? super T
.źródło
co jeśli użyjesz
źródło