В чем разница между а также ? - PullRequest
29 голосов
/ 05 марта 2020

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

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> в этом случае, и почему первая компиляция не выполняется?

Ответы [ 4 ]

14 голосов
/ 08 марта 2020

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

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

и вызвав его следующим образом:

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

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

Объявление класса generi c определяет набор параметризованных типов (§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)): </p>

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

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

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

  • ? супер T <=? </p>

  • ? супер T <=? расширяет Объект </p>

  • T <= T </p>

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

  • T <=? super T </p>

Это означает, что ? extends Number действительно содержит Integer или даже List<? extends Number> содержит List<Integer>, но это не относится к Map<Integer, List<? extends Number>> и Map<Integer, List<Integer>>. Больше об этом topi c можно найти в этой теме . Вы все еще можете заставить работать версию с подстановочным знаком ?, заявив, что вы ожидаете подтип 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>>());
    }
}
6 голосов
/ 05 марта 2020

В вызове:

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

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

UPDATE

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

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

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

2 голосов
/ 11 марта 2020

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

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>>, который является аргументом типа аргумента типа и сам имеет конкретное создание экземпляра типа generi c.

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

1 голос
/ 09 марта 2020

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

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

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, так что выражение List<Double> абсолютно нормально, также как и List<Integer>, верно?

Однако вы все еще думаете, что это Legal передать здесь 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
    }

В принципе, вы можете положить только 1038 *, поэтому безопасно вызывать этот метод с new HashMap<Integer, List<Integer>>() или new HashMap<Integer, List<Double>>() или new HashMap<Integer, List<Long>>() или new HashMap<Integer, List<Number>>().

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...