Kiedy ogólne elementy Java wymagają <? rozszerza T> zamiast <T> i czy jest jakaś wada przełączania?

205

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 assertThatsygnaturą 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ę assertThatpodpis metody na:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Następnie kompilacja działa.

Więc trzy pytania:

  1. 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ł.
  2. Czy zmiana assertThatmetody na ma jakiś minus Matcher<? extends T>? Czy są inne przypadki, które by się zepsuły, gdybyś to zrobił?
  3. Czy jest jakiś sens uogólnienia assertThatmetody w JUnit? MatcherKlasa 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, bo Matcherpo 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());
    }
}
Yishai
źródło
ten link jest bardzo przydatny (ogólne, dziedziczenie i podtyp): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Odpowiedzi:

145

Po pierwsze - muszę cię przekierować na http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - wykonuje niesamowitą robotę.

Podstawową ideą jest to, że używasz

<T extends SomeClass>

kiedy faktycznym parametrem może być SomeClassdowolny jego podtyp.

W twoim przykładzie

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

Mówisz, że expectedmogą zawierać obiekty klasy reprezentujące dowolną klasę, która implementuje Serializable. Twoja mapa wyników mówi, że może przechowywać tylko Dateobiekty klasy.

Kiedy przechodzą w wyniku tego, że masz ustawione Tdokładnie Mapod Stringdo Dateklasy obiektów, które nie odpowiadają Mapstanowi Stringna cokolwiek to jest Serializable.

Jedną rzecz do sprawdzenia - czy na pewno chcesz, Class<Date>a nie Date? Mapę Stringaby Class<Date>nie brzmi strasznie użyteczne w ogóle (wszystko może pomieścić jest Date.classjako wartości zamiast wystąpień Date)

Jeśli chodzi o uogólnianie assertThat, chodzi o to, że metoda może zapewnić Matcherprzekazanie wartości pasującej do typu wyniku.

Scott Stanchfield
źródło
W takim przypadku tak, chcę mapę klas. Podany przeze mnie przykład wymyślił użycie standardowych klas JDK zamiast własnych klas, ale w tym przypadku klasa jest faktycznie tworzona przez odbicie i używana na podstawie klucza. (Rozproszona aplikacja, w której klient nie ma dostępnych klas serwerów, tylko klucz której klasy należy użyć do pracy po stronie serwera).
Yishai
6
Zgaduję, że mój mózg utknął w tym, dlaczego mapa zawierająca klasy typu Date nie pasuje dobrze do rodzaju map zawierających klasy typu Serializable. Oczywiście klasy typu Serializable mogą być również innymi klasami, ale z pewnością obejmuje typ Data.
Yishai
W assertThat upewniając się, że rzutowanie jest wykonywane za Ciebie, metoda matcher.matches () nie ma znaczenia, więc skoro T nigdy nie jest używane, po co go włączać? (typ zwrotu metody jest nieważny)
Yishai
Ahhh - właśnie to dostaję za to, że nie przeczytałem definicji twierdzenia, że ​​jest wystarczająco blisko. Wygląda na to, że ma to zapewnić, że odpowiedni Matcher zostanie przekazany ...
Scott Stanchfield
28

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:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

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:

private <T> void genericAdd(T value, List<T> list)

Chce być w stanie wykonać oba:

T x = list.get(0);

i

list.add(value);

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

Matcher<? extends T>

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 kompilator Matcher<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.

Yishai
źródło
Nadal nie rozumiem, dlaczego nie mogę upcastować. Dlaczego nie mogę zamienić listy dat w listę serializowalną?
Thomas Ahle,
@ThomasAhle, ponieważ wówczas odniesienia, które uważają, że jest to lista dat, będą napotykać błędy rzutowania, gdy znajdą ciągi lub inne możliwe do serializacji.
Yishai,
Rozumiem, ale co się stanie, jeśli jakoś pozbędę się starej referencji, tak jakbym zwrócił ją List<Date>z metody typu List<Object>? To powinno być bezpieczne, nawet jeśli nie zezwala na to Java.
Thomas Ahle,
14

Sprowadza się do:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

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:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Jeśli jednak typ listy zostanie zmieniony? rozszerza T zamiast T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

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).

GreenieMeanie
źródło
9

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>()>do expected. Gdyby kompilator zezwolił na kompilację kodu, assertThat()przypuszczalnie zostałby uszkodzony, ponieważ oczekiwałby Dateobiektów zamiast Longobiektów znalezionych na mapie.

erickson
źródło
1
Nie całkiem podążam - kiedy mówisz „nie znaczy… ale…”: jaka jest różnica? (np. jaki byłby przykład „znanej, ale niespecyficznej” klasy, która pasuje do pierwszej definicji, ale nie do drugiej?)
poundifdef
Tak, to trochę niezręczne; nie jestem pewien, jak lepiej to wyrazić ... czy sensowniej jest powiedzieć „”? jest typem nieznanym, a nie typem, który coś pasuje? ”
erickson
1
To, co może pomóc w wyjaśnieniu, jest takie, że dla „dowolnej klasy rozszerzającej Serializowalny” można po prostu użyć<Serializable>
c0der
8

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).

GreenieMeanie
źródło
To z pewnością pomaga, ale „może wydawać się mylące” zastępuje się „dźwiękiem mylącym”. Jako uzupełnienie, dlaczego więc, zgodnie z tym wyjaśnieniem, <? extends T>kompiluje się metodę z Matcherem ?
Yishai
jeśli zdefiniujemy jako List <Serializable>, czy robi to samo dobrze? Mówię o twoim drugim akapicie. tj. polimorfizm by sobie z tym poradził?
Supun Wijerathne
3

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.Collectionsoferuje tę metodę:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

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 podklas T, 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 prostu Comparator<T>uczyni tę metodę użyteczną. Ale twórca tej metody uznał, że jeśli coś jest T, to jest to również instancja nadklasy T. Dlatego pozwala nam korzystać z Komparatora Tlub dowolnej nadklasy T, tj ? super T.

Lucas Ross
źródło
1

co jeśli użyjesz

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
źródło
Tak, na tym właśnie polegała moja powyższa odpowiedź.
GreenieMeanie
Nie, to nie pomaga w sytuacji, przynajmniej tak jak próbowałem.
Yishai