В чем разница между <? расширяет базу> и <T расширяет базу>?

29

В этом примере:

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() не может скомпилировать с:

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>>());
                      ^

пока compiles()принимается компилятором.

Этот ответ объясняет, что единственное отличие состоит в том <? ...>, что в отличие от этого , <T ...>вы можете ссылаться на тип позже, что, похоже, не так.

В чем разница между <? extends Number>и <T extends Number>в этом случае, и почему первая компиляция не выполняется?

Dev Null
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лев
[1] Java Общий тип: разница между списком <? extends Number> и List <T extends Number>, кажется, задают тот же вопрос, но, хотя это может представлять интерес, на самом деле это не дубликат. [2] Хотя это хороший вопрос, заголовок не отражает должным образом конкретный вопрос, задаваемый в последнем предложении.
Скомиса
Здесь уже есть ответ. Общий список Java <Список <? расширяет номер >>
Mạnh Quyết Nguyễn
Как насчет объяснения здесь ?
Jrook

Ответы:

14

Определив метод со следующей подписью:

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

и вызывая его так:

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

В jls §8.1.2 мы находим, что (интересная часть выделена мной):

Объявление универсального класса определяет набор параметризованных типов (§4.5), по одному для каждого возможного вызова раздела параметров типа аргументами типа . Все эти параметризованные типы совместно используют один и тот же класс во время выполнения.

Другими словами, тип Tсопоставляется с типом ввода и назначается Integer. Подпись эффективно станет static void compiles(Map<Integer, List<Integer>> map).

Когда дело доходит до doesntCompileметода, jls определяет правила подтипирования ( §4.5.1 , выделенный мной):

Говорят, что аргумент типа T1 содержит другой аргумент типа T2, записанный как T2 <= T1, если набор типов, обозначаемых через T2, является доказуемо подмножеством набора типов, обозначаемых через T1 при рефлексивном и транзитивном замыкании следующих правил ( где <: обозначает подтип (§4.10)):

  • ? расширяет T <=? расширяет S, если T <: S

  • ? расширяет T <=?

  • ? супер T <=? супер S если S <: T

  • ? супер T <=?

  • ? супер T <=? расширяет объект

  • Т <= Т

  • Т <=? расширяет T

  • Т <=? супер т

Это означает, что ? extends Numberдействительно содержит Integerили даже List<? extends Number>содержит List<Integer>, но это не относится к Map<Integer, List<? extends Number>>и Map<Integer, List<Integer>>. Больше на эту тему можно найти в этой теме . Вы все еще можете заставить ?работать версию с подстановочными знаками, объявив, что вы ожидаете подтип 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>>());
    }
}
Андроник
источник
[1] Я думаю, что вы имели в виду, ? extends Numberа не ? extends Numeric. [2] Ваше утверждение о том, что «это не относится к List <? Extends Number> и List <Integer>» , неверно. Как уже указывал @VinceEmigh, вы можете создать метод static void demo(List<? extends Number> lst) { }и вызывать его так demo(new ArrayList<Integer>());или иначе demo(new ArrayList<Float>());, и код компилируется и запускается нормально. Или я, возможно, неправильно понимаю или неправильно понимаю то, что вы заявили?
Скомиса
@ Вы правы в обоих случаях. Когда дело доходит до вашего второго пункта, я написал это обманчиво. Я имел List<? extends Number>в виду как параметр типа всей карты, а не самой. Большое спасибо за комментарий.
Андроник
@skomisa по той же причине List<Number>не содержит List<Integer>. Предположим, у вас есть функция static void check(List<Number> numbers) {}. Когда вызов с check(new ArrayList<Integer>());ним не компилируется, вы должны определить метод как static void check(List<? extends Number> numbers) {}. С картой то же самое, но с большей вложенностью.
Андроник
1
@skomisa, так же как Numberи параметр типа списка, и вы должны добавить его, ? extendsчтобы сделать его ковариантным, List<? extends Number>является параметром типа Mapи также нуждается ? extendsв ковариации.
Андроник
1
ХОРОШО. Так как вы предоставили решение для многоуровневого подстановочного знака (или «вложенного подстановочного знака» ?) И связались с соответствующей ссылкой JLS, получите награду.
скомиса
6

В звонилке:

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

T соответствует Integer, поэтому тип аргумента a Map<Integer,List<Integer>>. Это не относится к методу doesntCompile: тип аргумента остается Map<Integer, List<? extends Number>>тем же фактическим аргументом в вызове; и это не присваивается из HashMap<Integer, List<Integer>>.

ОБНОВИТЬ

В doesntCompileметоде ничто не мешает вам сделать что-то вроде этого:

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

Очевидно, что он не может принять HashMap<Integer, List<Integer>>аргумент в качестве аргумента.

Морис Перри
источник
Каким будет действительный звонок doesntCompile, тогда? Просто интересно об этом.
экстремальный байкер
1
@XtremeBiker doesntCompile(new HashMap<Integer, List<? extends Number>>());будет работать, как и раньше doesntCompile(new HashMap<>());.
скомиса
@XtremeBiker, даже это сработало бы, Map <Integer, List <? extends Number >> map = new HashMap <Целое число, Список <? расширяет номер >> (); map.put (null, new ArrayList <Integer> ()); doesntCompile (карта);
обезьяну
«это нельзя назначить из HashMap<Integer, List<Integer>>», не могли бы вы пояснить, почему это нельзя назначить из него?
Dev Null
@DevNull смотрите мое обновление выше
Морис Перри
2

Упрощенный пример демонстрации. Тот же пример можно визуализировать как ниже.

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>>является многоуровневым типом подстановочных знаков, тогда как List<? extends Number>является стандартным типом подстановочных знаков.

Допустимые конкретные экземпляры подстановочного типа List<? extends Number>включают в себя Numberи любые подтипы, Numberтогда как в случае, List<Pair<? extends Number>>когда это аргумент типа аргумента типа, а сам по себе имеет конкретный экземпляр универсального типа.

Дженерики являются инвариантами, поэтому Pair<? extends Number>можно использовать только групповые символы Pair<? extends Number>>. Внутренний тип ? extends Numberуже ковариантен. Вы должны сделать включающий тип ковариантным, чтобы разрешить ковариацию.

Сагар Вирам
источник
Как это <Pair<Integer>>не работает, <Pair<? extends Number>>но работает <T extends Number> <Pair<T>>?
jaco0646
@ jaco0646 По сути, вы задаете тот же вопрос, что и ОП, и ответ от Андроника был принят. Смотрите пример кода в этом ответе.
скомиса
@skomisa, да, я задаю тот же вопрос по нескольким причинам: одна из них заключается в том, что этот ответ на самом деле не отвечает на вопрос ОП; но во-вторых, мне легче понять этот ответ. Я не могу последовать ответу Андроника никоим образом, который приводит меня к пониманию вложенных, не вложенных обобщений или даже Tпротив ?. Часть проблемы заключается в том, что, когда Андроник достигает жизненно важной точки своего объяснения, он обращается к другой теме, которая использует только тривиальные примеры. Я надеялся получить более четкий и полный ответ здесь.
jaco0646
1
@ jaco0646 ОК. В документе «Часто задаваемые вопросы по обобщению Java - аргументы типа» Анджелики Лангер есть часто задаваемый вопрос под названием « Что означают многоуровневые (т. Е. Вложенные) символы подстановки? , Это лучший из известных мне источников для объяснения вопросов, поднятых в вопросе ФП. Правила для вложенных подстановочных знаков не являются ни прямыми, ни интуитивными.
скомиса
1

Я бы порекомендовал вам ознакомиться с документацией об универсальных подстановочных знаках, особенно с инструкциями по их использованию.

Откровенно говоря ваш метод #doesntCompile

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

и звоните как

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

В корне неверно

Добавим юридическую реализацию:

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

Это действительно хорошо, потому что Double расширяет Number, так что put List<Double>тоже отлично List<Integer>, верно?

Тем не менее, вы все еще думаете, что это законно, чтобы перейти new HashMap<Integer, List<Integer>>()от вашего примера?

Компилятор так не считает, и делает все возможное, чтобы избежать подобных ситуаций.

Попробуйте сделать ту же реализацию с методом #compile, и компилятор, очевидно, не позволит вам поместить список двойников в карту.

    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
    }

По сути, вы ничего не можете поместить, но List<T>поэтому безопасно вызывать этот метод с помощью new HashMap<Integer, List<Integer>>()или new HashMap<Integer, List<Double>>()или new HashMap<Integer, List<Long>>()или new HashMap<Integer, List<Number>>().

Итак, в двух словах, вы пытаетесь обмануть с помощью компилятора, и он справедливо защищает от такого обмана.

NB: ответ Мориса Перри абсолютно верный. Я просто не уверен, что это достаточно ясно, поэтому постарался (очень надеюсь, что мне удалось) добавить более обширный пост.

Махно
источник