Jaka jest różnica między <? rozszerza bazę> i <T rozszerza bazę>?

29

W tym przykładzie:

import java.util.*;

public class Example {
    static void doesntCompile(Map<Integer, List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    static void function(List<? extends Number> outer)
    {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}

doesntCompile() nie można skompilować z:

Example.java:9: error: incompatible types: HashMap<Integer,List<Integer>> cannot be converted to Map<Integer,List<? extends Number>>
        doesntCompile(new HashMap<Integer, List<Integer>>());
                      ^

podczas gdy compiles()jest akceptowany przez kompilator.

Ta odpowiedź wyjaśnia, że ​​jedyną różnicą jest to, że w przeciwieństwie do <? ...>, <T ...>pozwala odwoływać się do typu później, co nie wydaje się być prawdą.

Jaka jest różnica między <? extends Number>iw <T extends Number>tym przypadku i dlaczego pierwsza kompilacja nie jest wykonywana?

Dev Null
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew
[1] Java Rodzaj ogólny: różnica między List <? Wydłuża liczbę> i Lista <T wydłuża liczbę> wydaje się zadawać to samo pytanie, ale chociaż może być interesujące, tak naprawdę nie jest duplikatem. [2] Chociaż jest to dobre pytanie, tytuł nie odzwierciedla właściwie konkretnego pytania zadanego w ostatnim zdaniu.
skomisa
Tutaj jest już udzielona odpowiedź Ogólna lista Java <Lista <? rozszerza liczbę >>
Mạnh Quyết Nguyễn
A może wyjaśnienie tutaj ?
jrook

Odpowiedzi:

14

Definiując metodę z następującym podpisem:

static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

i wywoływanie go w następujący sposób:

compiles(new HashMap<Integer, List<Integer>>());

W jls §8.1.2 stwierdzamy, że (interesująca część pogrubiona przeze mnie):

Ogólna deklaracja klasy definiuje zestaw sparametryzowanych typów (§ 4.5), po jednym dla każdego możliwego wywołania sekcji parametru typu według argumentów typu . Wszystkie te sparametryzowane typy dzielą tę samą klasę w czasie wykonywania.

Innymi słowy, typ Tjest dopasowywany do typu wejściowego i przypisywany Integer. Podpis stanie się skutecznie static void compiles(Map<Integer, List<Integer>> map).

Jeśli chodzi o doesntCompilemetodę, jls określa zasady subtypowania ( pogrubione przeze mnie p. 4.5.1 ):

Mówi się, że argument typu T1 zawiera inny argument typu T2, zapisany T2 <= T1, jeśli zbiór typów oznaczonych przez T2 jest możliwym podzbiorem zbioru typów oznaczonych przez T1 pod zwrotnym i przechodnim zamknięciem następujących reguł ( gdzie <: oznacza podtyp (§4.10)):

  • ? rozszerza T <=? rozszerza S, jeśli T <: S

  • ? rozszerza T <=?

  • ? super T <=? super S, jeśli S <: T

  • ? super T <=?

  • ? super T <=? rozszerza Object

  • T <= T

  • T <=? rozszerza T

  • T <=? super T.

Oznacza to, że ? extends Numberrzeczywiście zawiera Integerlub nawet List<? extends Number>zawiera List<Integer>, ale nie jest tak w przypadku Map<Integer, List<? extends Number>>i Map<Integer, List<Integer>>. Więcej na ten temat można znaleźć w tym wątku SO . Nadal możesz sprawić, by wersja ze ?znakiem wieloznacznym działała, deklarując, że spodziewasz się podtypu List<? extends Number>:

public class Example {
    // now it compiles
    static void doesntCompile(Map<Integer, ? extends List<? extends Number>> map) {}
    static <T extends Number> void compiles(Map<Integer, List<T>> map) {}

    public static void main(String[] args) {
        doesntCompile(new HashMap<Integer, List<Integer>>());
        compiles(new HashMap<Integer, List<Integer>>());
    }
}
Andronikus
źródło
[1] Myślę, że miałeś na myśli ? extends Numberraczej niż ? extends Numeric. [2] Twoje twierdzenie, że „nie jest tak w przypadku List <? Extends Number> i List <Integer>” jest błędne. Jak już wskazał @VinceEmigh, możesz stworzyć metodę static void demo(List<? extends Number> lst) { }i wywołać ją w ten demo(new ArrayList<Integer>());lub inny sposób demo(new ArrayList<Float>());, a kod kompiluje się i działa OK. A może źle interpretuję lub nie rozumiem tego, co powiedziałeś?
skomisa
@ Masz rację w obu przypadkach. Jeśli chodzi o twój drugi punkt, napisałem go w sposób wprowadzający w błąd. Miałem na myśli List<? extends Number>jako parametr typu całej mapy, a nie samej. Dziękuję bardzo za komentarz.
Andronicus
@skomisa z tego samego powodu List<Number>nie zawiera List<Integer>. Załóżmy, że masz funkcję static void check(List<Number> numbers) {}. Gdy wywoływanie check(new ArrayList<Integer>());go nie kompiluje, musisz zdefiniować metodę jako static void check(List<? extends Number> numbers) {}. Z mapą jest tak samo, ale z większą liczbą zagnieżdżeń.
Andronicus
1
@skomisa as Numberjest parametrem typu listy i należy go dodać, ? extendsaby był kowariantem, List<? extends Number>jest parametrem typu Mapi również wymaga ? extendskowariancji.
Andronicus
1
OK. Ponieważ dostarczyłeś rozwiązanie dla wielopoziomowego symbolu wieloznacznego (zwanego także „zagnieżdżonym znakiem wieloznacznym” ?) I powiązanego z odpowiednim odniesieniem JLS, otrzymaj nagrodę.
skomisa
6

W rozmowie:

compiles(new HashMap<Integer, List<Integer>>());

T jest dopasowane do liczby całkowitej, więc typ argumentu to Map<Integer,List<Integer>>. W przypadku tej metody tak nie jest doesntCompile: typ argumentu pozostajeMap<Integer, List<? extends Number>> bez względu na faktyczny argument w wywołaniu; i nie można tego przypisać HashMap<Integer, List<Integer>>.

AKTUALIZACJA

W doesntCompilemetodzie nic nie stoi na przeszkodzie, aby zrobić coś takiego:

static void doesntCompile(Map<Integer, List<? extends Number>> map) {
    map.put(1, new ArrayList<Double>());
}

Oczywiście nie może przyjąć HashMap<Integer, List<Integer>>argumentu.

Maurice Perry
źródło
Jakie byłoby zatem prawidłowe wezwanie doesntCompile? Po prostu ciekawi mnie to.
Xtreme Biker
1
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());działałby tak samo doesntCompile(new HashMap<>());.
skomisa
@XtremeBiker, nawet to by działało, Mapa <Liczba całkowita, Lista <? rozszerza Number >> map = new HashMap <Integer, List <? rozszerza Number >> (); map.put (null, new ArrayList <Integer> ()); doesntCompile (mapa);
MOnkey
„nie można tego przypisać HashMap<Integer, List<Integer>>”. Czy mógłbyś wyjaśnić, dlaczego nie można tego przypisać?
Dev Null
@DevNull zobacz moją aktualizację powyżej
Maurice Perry
2

Uproszczony przykład demonstracji. Ten sam przykład można wizualizować jak poniżej.

static void demo(List<Pair<? extends Number>> lst) {} // doesn't work
static void demo(List<? extends Pair<? extends Number>> lst) {} // works
demo(new ArrayList<Pair<Integer>()); // works
demo(new ArrayList<SubPair<Integer>()); // works for subtype too

public static class Pair<T> {}
public static class SubPair<T> extends Pair<T> {}

List<Pair<? extends Number>>jest wielopoziomowym typem symboli wieloznacznych, podczas gdy List<? extends Number>jest standardowym typem symboli wieloznacznych.

Prawidłowe konkretne instancje typu dzikiej karty List<? extends Number>obejmują Numberi wszelkie podtypy, Numberpodczas gdy w przypadku List<Pair<? extends Number>>których jest argumentem typu argument typu i sam ma konkretną instancję typu ogólnego.

Generyczne są niezmienne, więc Pair<? extends Number>typ wieloznaczny może tylko zaakceptować Pair<? extends Number>>. Typ wewnętrzny ? extends Numberjest już kowariantny. Musisz ustawić typ zamykający jako kowariant, aby umożliwić kowariancję.

Sagar Veeram
źródło
Jak to <Pair<Integer>>działa, <Pair<? extends Number>>ale nie działa <T extends Number> <Pair<T>>?
jaco0646
@ jaco0646 Zasadniczo zadajesz to samo pytanie co OP, a odpowiedź Andronicusa została zaakceptowana. Zobacz przykładowy kod w tej odpowiedzi.
skomisa
@skomisa, tak, zadaję to samo pytanie z kilku powodów: po pierwsze, ta odpowiedź nie wydaje się odpowiadać na pytanie PO; ale po drugie, odpowiedź ta jest dla mnie łatwiejsza do zrozumienia. Nie mogę śledzić odpowiedzi od Andronikus w jakikolwiek sposób, który prowadzi mnie do zrozumienia zagnieżdżonych vs non-zagnieżdżonych generycznych lub nawet Tvs ?. Część problemu polega na tym, że kiedy Andronik osiąga zasadniczy punkt swojego wyjaśnienia, przechodzi do innego wątku, który wykorzystuje tylko trywialne przykłady. Miałem nadzieję, że otrzymam tutaj bardziej przejrzystą i kompletną odpowiedź.
jaco0646
1
@ jaco0646 OK. Dokument „Często zadawane pytania dotyczące Java Generics - Argumenty typów” autorstwa Angeliki Langer zawiera FAQ zatytułowany Co oznaczają symbole wielopoziomowe (tj. Zagnieżdżone)? . To najlepsze źródło, jakie znam do wyjaśniania kwestii poruszonych w pytaniu PO. Zasady zagnieżdżonych symboli wieloznacznych nie są ani proste, ani intuicyjne.
skomisa
1

Polecam zajrzeć do dokumentacji ogólnych symboli wieloznacznych, a zwłaszcza wskazówek dotyczących używania symboli wieloznacznych

Szczerze mówiąc, twoja metoda #doesntCompile

static void doesntCompile(Map<Integer, List<? extends Number>> map) {}

i zadzwoń jak

doesntCompile(new HashMap<Integer, List<Integer>>());

Jest zasadniczo niepoprawny

Dodajmy prawne wdrożenie:

    static void doesntCompile(Map<Integer, List<? extends Number>> map) {
        List<Double> list = new ArrayList<>();
        list.add(0.);
        map.put(0, list);
    }

Jest naprawdę w porządku, ponieważ Double rozszerza liczbę, więc ułożenie List<Double>jest absolutnie w porządku List<Integer>, prawda?

Czy jednak nadal uważasz, że legalne jest przekazywanie tutaj new HashMap<Integer, List<Integer>>()ze swojego przykładu?

Kompilator tak nie uważa i robi wszystko, aby uniknąć takich sytuacji.

Spróbuj wykonać tę samą implementację za pomocą metody #compile, a kompilator oczywiście nie pozwoli Ci umieścić listy podwójnych na mapie.

    static <T extends Number> void compiles(Map<Integer, List<T>> map) {
        List<Double> list = new ArrayList<>();
        list.add(10.);
        map.put(10, list); // does not compile
    }

Zasadniczo nic nie możesz umieścić, ale List<T>dlatego bezpiecznie jest wywoływać tę metodę za pomocą new HashMap<Integer, List<Integer>>()lub new HashMap<Integer, List<Double>>()lub new HashMap<Integer, List<Long>>()lub new HashMap<Integer, List<Number>>().

Krótko mówiąc, próbujesz oszukiwać za pomocą kompilatora, który dość dobrze broni się przed takim oszustwem.

Uwaga: odpowiedź wysłana przez Maurice'a Perry'ego jest całkowicie poprawna. Po prostu nie jestem pewien, czy to wystarczająco jasne, więc próbowałem (naprawdę mam nadzieję, że udało mi się) dodać bardziej obszerny post.

Machno
źródło