Подтвердить что ты не робот

Как работают дженерики дженериков?

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

У меня есть следующий класс

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

Строка 4 дает мне ошибку

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

Очевидно, компилятор считает, что разные ? на самом деле не равны. В то время как мое чувство кишки говорит мне, это правильно.

Может ли кто-нибудь предоставить пример, где я бы получил ошибку времени выполнения, если строка 4 была законной?

EDIT:

Чтобы избежать путаницы, я заменил =null в строке 3 на конкретное назначение

4b9b3361

Ответ 1

Как заметил Кенни в своем комментарии, вы можете обойти это:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Это немедленно говорит нам, что операция не небезопасная, она просто жертва ограниченного вывода. Если бы это было небезопасно, вышеупомянутое не собиралось.

Поскольку использование явных параметров типа в обобщенном методе, как указано выше, необходимо когда-либо использовать как подсказку, мы можем предположить, что здесь требуется техническое ограничение механизма вывода. В самом деле, компилятор Java 8 в настоящее время планируется добавить несколько улучшений для ввода-вывода типов. Я не уверен, будет ли разрешен ваш конкретный случай.

Итак, что на самом деле происходит?

Ну, ошибка компиляции, которую мы получаем, показывает, что параметр типа T of Collections.singletonList определяется как capture<Test<? extends Number>>. Другими словами, подстановочный знак содержит некоторые связанные с ним метаданные, которые связывают его с конкретным контекстом.

  • Лучший способ думать о захвате подстановочного знака (capture<? extends Foo>) - это как неопределенный параметр типа тех же границ (т.е. <T extends Foo>, но без возможности ссылки T).
  • Лучший способ "развязать" мощность захвата - связать его с параметром именованного типа общего метода. Я продемонстрирую это в примере ниже. См. Учебник по Java "Подстановочный шаблон и вспомогательные методы" (спасибо за ссылку @WChargin) для дальнейшего чтения.

Предположим, что мы хотим иметь метод, который перемещает список, обертывая назад. Тогда предположим, что наш список имеет неизвестный (групповой) тип.

public static void main(String... args) {
    List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    List<? extends String> cycledTwice = cycle(cycle(list));
}

public static <T> List<T> cycle(List<T> list) {
    list.add(list.remove(0));
    return list;
}

Это отлично работает, потому что T разрешено capture<? extends String>, а не ? extends String. Если бы вместо этого использовалась не-общая реализация цикла:

public static List<? extends String> cycle(List<? extends String> list) {
    list.add(list.remove(0));
    return list;
}

Он не смог бы скомпилировать, потому что мы не сделали захват доступным, назначив его параметру типа.

Итак, это начинает объяснять, почему потребитель singletonList выиграл бы от разрешения типа-инфердера T до Test<capture<? extends Number> и, таким образом, возвратил List<Test<capture<? extends Number>>> вместо List<Test<? extends Number>>.

Но почему одно не назначается другому?

Почему мы не можем просто назначить List<Test<capture<? extends Number>>> List<Test<? extends Number>>?

Хорошо, если мы подумаем о том, что capture<? extends Number> является эквивалентом параметра анонимного типа с верхней границей Number, тогда мы можем включить этот вопрос в "Почему не следующий компилятор?" (это не так!):

public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
    return t;
} 

У этого есть веская причина не компиляции. Если бы это было так, то это было бы возможно:

//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);

Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>

Так почему же явная работа?

Пусть цикл вернется к началу. Если вышеуказанное небезопасно, то почему это разрешено:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Для этого это означает, что разрешено следующее:

Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;

Ну, это недопустимый синтаксис, так как мы не можем напрямую ссылаться на захват, поэтому давайте оценим его, используя ту же технику, что и выше! Пусть привязка захвата к другому варианту "присваивать":

public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
    return t;
} 

Это успешно компилируется. И нетрудно понять, почему это должно быть безопасно. Это очень практичный пример чего-то вроде

List<? extends Number> l = new List<Double>();

Ответ 2

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

Следовательно, если вы удалите вывод из вызова singletonList, указав <T> для него:

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

Он отлично работает. Сгенерированный код не отличается от того, был ли ваш вызов законным, это просто ограничение компилятора, что он не может понять это самостоятельно.

Правило, что вывод, создающий захват и захват, несовместим, - это то, что останавливает этот пример учебника от компиляции, а затем взрывается во время выполнения:

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

Да, спецификация языка и компилятор, возможно, могут быть более сложными, чтобы рассказать о вашем примере помимо этого, но это не так, и это достаточно просто, чтобы обойти.

Ответ 3

Причина в том, что компилятор не знает, что ваши подстановочные типы одного типа.

Он также не знает, что ваш экземпляр null. Хотя null является членом всех типов, компилятор рассматривает только объявленные типы, а не то, что может содержать значение переменной при проверке типов.

Если код выполнен, это не вызовет исключения, но это только потому, что значение равно null. По-прежнему существует несоответствие потенциального типа и что задание компилятора - запретить несоответствие типов.

Ответ 4

Посмотрите тип стирания. Проблема заключается в том, что "время компиляции" - единственная возможность, которую Java должна обеспечивать соблюдение этих дженериков, поэтому, если бы она позволяла этому через нее не была в состоянии сказать, пытались ли вы вставить что-то недействительное. Это на самом деле хорошо, потому что это означает, что после компиляции программы Generics не подвергаются какому-либо снижению производительности во время выполнения.

Попробуем и посмотрим на ваш пример другим способом (пусть используют два типа, которые расширяют число, но ведут себя по-разному). Рассмотрим следующую программу:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<BigDecimal>> l = Collections.singletonList(t);
    for(q16449799<BigDecimal> i : l) {
      System.out.println(i.val);
    }
  }
}

Этот вывод (как и следовало ожидать):

3.141592653589793115997963468544185161590576171875

Теперь, предполагая, что представленный код не вызвал ошибку компилятора:

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<AtomicLong>> l = Collections.singletonList(t);
    for(q16449799<AtomicLong> i : l) {
      System.out.println(i.val);
    }
  }
}

Что вы ожидаете от вывода? Вы не можете разумно отличить BigDecimal от AtomicLong (вы могли бы построить AtomicLong из значения BigDecimal, но кастинг и построение - это разные вещи, а Generics реализованы в качестве сахара времени компиляции, чтобы убедиться, что отливки успешны). Что касается комментария @KennyTM, то конкретный тип ищет в исходном примере, но попытайтесь скомпилировать это:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T> {
  public T val;

  public static void main(String ... args) {
    q16449799<? extends Number> t = new q16449799<BigDecimal>();

    t.val = new BigDecimal(Math.PI);

    List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
    for(q16449799<? extends Number> i : l) {
      System.out.println(i.val);
    }
  }
}

Это приведет к ошибке в момент, когда вы попытаетесь установить значение t.val.

Ответ 5

Возможно, это может объяснить проблему компилятора:

List<? extends Number> myNums = new ArrayList<Integer>();

Этот список подстановочных символов может содержать любые элементы, простирающиеся от числа. Так что его ОК, чтобы назначить ему список Integer. Однако теперь я мог бы добавить Double to myNums, потому что Double также распространяется на Number, что приведет к проблеме времени выполнения. Поэтому компилятор запрещает каждый доступ на запись к myNums, и я могу использовать только методы чтения на нем, потому что я знаю только, что когда-либо получаю, может быть добавлено в Number.

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

Но, к счастью, есть хитрость, чтобы обойти эту ошибку, чтобы вы могли самостоятельно проверить, что может сломать это:

public static void main(String[] args) {

    List<? extends Number> list1 = new ArrayList<BigDecimal>();
    List<List<? extends Number>> list2 = copyHelper(list1);


}

private static <T> List<List<T>> copyHelper(List<T> list) {
    return Collections.singletonList(list);

}