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