Kiedy Java generics wymaga zamiast i czy jest jakiś minus przełączania?

Podano następujący przykład (używając JUnit z matcherami Hamcrest):

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

Nie kompiluje się z podpisem metody JUnit assertThat:

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ę podpis metody assertThat na:

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

Wtedy kompilacja działa.

Więc trzy pytania:

  1. dlaczego aktualna wersja nie jest kompilowana? Chociaż niejasno Rozumiem tutaj kwestie kowariancji, nie mógłbym tego wyjaśnić, gdybym musiał.
  2. czy jest jakiś minus W zmianie metody assertThat na Matcher<? extends T>? Czy są inne sprawy, które by się załamały, gdybyś to zrobił?
  3. czy jest jakiś sens generyzacji metody assertThat W JUnit? Klasa Matcher nie wydaje się tego wymagać, ponieważ JUnit wywołuje metodę Match, która nie jest wpisywana za pomocą żadnego generycznego, i wygląda jak próba wymuszenia bezpieczeństwa typu, które nic nie robi, ponieważ Matcher po prostu nie będzie pasować, i test się nie powiedzie. Żadnych niebezpiecznych operacji (lub 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());
    }
}
Author: blacktide, 2009-05-22

7 answers

Po pierwsze - muszę skierować cię do http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html -- wykonuje niesamowitą pracę.

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

<T extends SomeClass>

Gdy rzeczywistym parametrem może być SomeClass lub dowolny 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 expected może zawierać obiekty klasy, które reprezentują dowolną klasę implementującą Serializable. Mapa wyników mówi, że może przechowywać tylko obiekty klasy Date.

Kiedy w rezultacie ustawiasz T dokładnie Map z String do Date obiektów klasy, które nie pasują Map z String do niczego, co jest Serializable.

Jedna rzecz do sprawdzenia -- czy na pewno chcesz Class<Date> a nie Date? Mapa String do Class<Date> nie brzmi zbyt użytecznie w ogóle (wszystko, co może pomieścić to Date.class jako wartości, a nie instancje Date)

Jeśli chodzi o generyzację assertThat, chodzi o to, że metoda może zapewnić, że Matcher pasujący do typu wyniku zostanie przekazany do środka.

 114
Author: Scott Stanchfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-10 00:44:20

Dzięki wszystkim, którzy odpowiedzieli na pytanie, to naprawdę pomogło mi wyjaśnić rzeczy. W końcu odpowiedź Scotta Stanchfielda była najbliższa temu, w jaki sposób skończyło się na zrozumieniu go, ale ponieważ nie rozumiałem go, kiedy po raz pierwszy go napisał, staram się ponownie wyjaśnić problem, aby miejmy nadzieję, że ktoś inny skorzysta.

Powtórzę pytanie w kategoriach listy, ponieważ ma tylko jeden parametr ogólny i to ułatwi zrozumienie.

Celem parametryzowana Klasa (taka jak List <Date> lub Map<K, V> jak w przykładzie) ma wymusić downcast i mieć gwarancję, że jest to bezpieczne (bez wyjątków runtime).

Rozważmy przypadek listy. Istotą mojego pytania jest to, dlaczego metoda, która przyjmuje typ T i Listę, nie akceptuje listy czegoś dalej w łańcuchu dziedziczenia niż T. rozważ 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 nie będzie kompilowane, ponieważ parametr list jest listą daty, a nie lista ciągów. Generyki nie byłyby zbyt użyteczne, gdyby to się skompilowało.

To samo dotyczy mapy {[7] } nie jest to to samo co Mapa <String, Class<java.util.Date>>. Nie są one kowariantne, więc jeśli chciałem wziąć wartość z mapy zawierającej klasy daty i umieścić ją na mapie zawierającej elementy serializowalne, to jest w porządku, ale podpis metody mówiący: {]}

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

Chce być w stanie zrobić jedno i drugie:

T x = list.get(0);

I

list.add(value);

W tym przypadku, mimo że metoda junit nie dba o te rzeczy, podpis metody wymaga kowariancji, której nie otrzymuje, dlatego nie kompiluje.

W pytaniu drugim,

Matcher<? extends T>

Ma tę minusę, że naprawdę akceptuje wszystko, gdy T jest obiektem, co nie jest intencją API. Celem jest statyczne upewnienie się, że matcher pasuje do rzeczywistego obiektu i nie ma sposobu, aby wykluczyć obiekt z tego obliczenia.

Odpowiedź na trzecie pytanie brzmi, że nic nie zostanie utracone, jeśli chodzi o niezaznaczoną funkcjonalność (nie byłoby niebezpiecznego typecastingu w API JUnit, gdyby ta metoda nie była generyczna), ale starają się osiągnąć coś innego-statycznie upewnić się, że oba parametry są prawdopodobne, aby pasowały.

Edycja (po dalszej kontemplacji i doświadczeniu):

Jednym z większych problemów z podpisem metody assertThat jest próba zrównania zmiennej T z parametrem ogólnym T. że nie działa, ponieważ nie są kowariantne. Na przykład możesz mieć T, które jest List<String>, ale następnie przekazać dopasowanie, które kompilator wypracuje do Matcher<ArrayList<T>>. Teraz, jeśli nie byłby to parametr typu, wszystko byłoby dobrze, ponieważ List i ArrayList są kowariantne, ale ponieważ Generyki, jeśli chodzi o kompilator wymaga ArrayList, nie może tolerować listy z powodów, które mam nadzieję są jasne z powyższego.

 22
Author: Yishai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-30 14:07:35

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 Referencja klasy c1 może zawierać długą instancję (ponieważ obiekt bazowy w danym czasie mógł być List<Long>), ale oczywiście nie może być przypisana do daty, ponieważ nie ma gwarancji, że "nieznana" klasa była datą. Nie jest to typsesafe, więc kompilator go nie zezwala.

Jeśli jednak wprowadzimy jakiś inny obiekt, powiedzmy List (w twoim przykładzie ten obiekt to Matcher), to stanie się prawdą:

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

...Jednakże, jeśli Typ listy stanie się ? 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 przypisania l1 = l2;

Zagnieżdżone symbole wieloznaczne nadal są bardzo mylące, ale mam nadzieję, że ma to sens, dlaczego pomaga zrozumieć generyki, patrząc na to, jak można przypisać do siebie ogólne odniesienia. Jest to również bardziej mylące, ponieważ kompilator wnioskuje Typ T, gdy wykonujesz wywołanie funkcji (jesteś nie mówiąc wprost, że to T jest).

 11
Author: GreenieMeanie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-22 17:29:55

Powodem, dla którego twój oryginalny kod nie kompiluje się jest to, że <? extends Serializable> nie oznacza "każdą klasę, która rozszerza Serializowalną", ale " jakąś nieznaną, ale specyficzną klasę, która rozszerza Serializowalną."

Na przykład, biorąc pod uwagę kod tak, jak został napisany, jest całkowicie poprawne przypisanie new TreeMap<String, Long.class>()> do expected. Jeśli kompilator zezwolił na kompilację kodu, assertThat() prawdopodobnie uległby awarii, ponieważ oczekiwałby obiektów Date zamiast obiektów Long znajdujących się na mapie.

 8
Author: erickson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-02 08:08:06

Jednym ze sposobów, aby zrozumieć symbole wieloznaczne, jest myślenie, że symbole wieloznaczne nie określają typu możliwych obiektów, które Dane ogólne odniesienie może "mieć", ale Typ innych ogólnych odniesień, z którymi jest zgodne (może to zabrzmieć myląco...) Jako taka, pierwsza odpowiedź jest bardzo myląca w jego sformułowaniu.

Innymi słowy, List<? extends Serializable> oznacza, że możesz przypisać to odniesienie do innych list, gdzie typ jest jakimś nieznanym typem, który jest lub jest podklasą Serializowalną. NIE pomyśl o tym w kategoriach pojedynczej listy zdolnej do przechowywania podklas Serializowalnych (ponieważ jest to niepoprawna semantyka i prowadzi do nieporozumień generycznych).

 6
Author: GreenieMeanie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-22 15:58:17

Wiem, że to stare pytanie, ale chcę podzielić się przykładem, który myślę, że całkiem dobrze wyjaśnia ograniczenia wildcards. java.util.Collections oferuje 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 rozszerzających T. Jeśli lista zawiera Zwierzęta, lista może zawierać zarówno psy ,jak i koty (oba zwierzęta). Psy mają własność "woofVolume" i koty mają własność " meowVolume."Chociaż możemy chcieć sortować w oparciu o te właściwości szczególnie podklasy T, Jak możemy oczekiwać, że ta metoda to zrobi? Ograniczenie komparatora polega na tym, że może porównywać tylko dwie rzeczy jednego typu (T). Tak więc, Wymaganie po prostu Comparator<T> uczyniłoby tę metodę użyteczną. Jednak twórca tej metody uznał, że jeśli coś jest T, to jest również instancją superklas T. Dlatego pozwala nam używać komparatora T lub dowolnej superklasy T, tj. ? super T.

 3
Author: Lucas Ross,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-24 07:03:01

Co jeśli użyjesz

Map<String, ? extends Class<? extends Serializable>> expected = null;
 1
Author: newacct,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-22 17:30:03