Java Generics Puzzler, расширяющий класс и использование подстановочных знаков

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

import java.util.*;

class Tbin<T> extends ArrayList<T> {}
class TbinList<T> extends ArrayList<Tbin<T>> {}

class Base {}
class Derived extends Base {}

public class Test {
  public static void main(String[] args) {
    ArrayList<Tbin<? extends Base>> test = new ArrayList<>();
    test.add(new Tbin<Derived>());

    TbinList<? extends Base> test2 = new TbinList<>();
    test2.add(new Tbin<Derived>());
  }
}

Использование Java 8. Мне кажется, что прямое создание контейнера в test эквивалентно контейнеру в test2, но компилятор говорит:

Test.java:15: error: no suitable method found for add(Tbin<Derived>)
    test2.add(new Tbin<Derived>());
         ^

Как написать Tbin и TbinList, чтобы последняя строка была приемлемой?

Обратите внимание, что на самом деле я добавляю типизированный Tbin, поэтому я указал Tbin<Derived> в последней строке.

Ответ 1

ОК, вот ответ:

import java.util.*;

class Tbin<T> extends ArrayList<T> {}
class TbinList<T> extends ArrayList<Tbin<? extends T>> {}

class Base {}
class Derived extends Base {}

public class Test {
  public static void main(String[] args) {

    TbinList<Base> test3 = new TbinList<>();
    test3.add(new Tbin<Derived>());

  }
}

Как я и ожидал, это было очевидно, когда я это увидел. Но много хлопот, чтобы добраться сюда. Генераторы Java кажутся простыми, если вы смотрите только на рабочий код.

Спасибо, все, за звучание.

Ответ 2

Это происходит из-за способа конверсии захвата:

Существует преобразование захвата из параметризованного типа G<T1,...,Tn> в параметризованный тип G<S1,...,Sn>, где для 1 ≤ я ≤ n:

  • Если Ti является аргументом типа подстановки формы ? extends Bi, то Si является новой переменной типа [...].

Преобразование захвата не применяется рекурсивно.

Обратите внимание на бит окончания. Итак, это означает, что при заданном типе:

    Map<?, List<?>>
//      │  │    └ no capture (not applied recursively)
//      │  └ T2 is not a wildcard
//      └ T1 is a wildcard

Захватываются только "внешние" подстановочные знаки. Подстановочный знак Map зафиксирован, но подстановочный элемент List нет. Вот почему, например, мы можем добавить к List<List<?>>, но не к List<?>. Важно отметить размещение шаблона.

Переведя это на TbinList, если у нас есть ArrayList<Tbin<?>>, подстановочный знак находится в месте, где он не захватывается, но если у нас есть TbinList<?>, подстановочный знак находится в месте, где он захвачена.

Как я упоминал в комментариях, один очень интересный тест:

ArrayList<Tbin<? extends Base>> test3 = new TbinList<>();

Мы получаем эту ошибку:

error: incompatible types: cannot infer type arguments for TbinList<>
    ArrayList<Tbin<? extends Base>> test3 = new TbinList<>();
                                                        ^
    reason: no instance(s) of type variable(s) T exist so that
            TbinList<T> conforms to ArrayList<Tbin<? extends Base>>

Поэтому нет способа заставить его работать как есть. Необходимо изменить одно из объявлений классов.


Кроме того, подумайте об этом таким образом.

Предположим, что мы имели:

class Derived1 extends Base {}
class Derived2 extends Base {}

И поскольку подстановочный знак позволяет подтипирование, мы можем это сделать:

TbinList<? extends Base> test4 = new TbinList<Derived1>();

Можем ли мы добавить Tbin<Derived2> в test4? Нет, это будет загрязнение кучи. В TbinList<Derived1> мы могли бы опуститься Derived2.

Ответ 3

Заменив определение TbinList на

class TbinList<T> extends ArrayList<Tbin<? extends T>> {}

и определяя test2 с помощью

TbinList<Base> test2 = new TbinList<>();

вместо этого решит проблему.

С вашим определением вы получите ArrayList<Tbin<T>>, где T - любой фиксированный класс, расширяющий Base.

Ответ 4

Вы используете ограниченный шаблон (TbinList<? extends Base>> ...). Этот шаблон не позволит вам добавлять какие-либо элементы в список. Если вы хотите получить дополнительную информацию, прочитайте раздел Wildcards в документах.

Ответ 5

вы не можете добавить какие-либо объекты в TbinList<? extends Base>, не гарантируется, какие объекты вы вставляете в список. Предполагается читать данные из test2, когда вы используете подстановочный знак extends

Если вы объявили как TbinList<? extends Base>, что означает, что это любой подкласс класса Base или класса Base, и когда вы его инициализируете, вы используете алмаз, отличный от имени конкретного класса, что делает ваш test2 не очевидным, что делает сложнее сказать, какие объекты можно вставить. Мое предложение состоит в том, чтобы избежать такого объявления, это опасно, у него могут не быть ошибки компиляции, но это ужасный код, вы можете что-то добавить, но вы также можете добавить НЕПРАВИЛЬНУЮ вещь, которая нарушит ваш код.

Ответ 6

Вы можете определить общие типы следующим образом:

class Tbin<T> extends ArrayList<T> {}
class TbinList<K, T extends Tbin<K>> extends ArrayList<T> {}

Затем вы должны создать экземпляр типа:

TbinList<? extends Base, Tbin<? extends Base>> test2 = new TbinList<>();
test2.add(new Tbin<Derived>());