Связывающие дженерики с ключевым словом "super"
Почему я могу использовать super
только с подстановочными знаками, а не с параметрами типа?
Например, в Collection
интерфейс, почему toArray
метод написан не так
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
6 ответов
super
привязать параметр именованного типа (например, <S super T>
) в отличие от подстановочного знака (например, <? super T>
) является НЕЗАКОННЫМ просто потому, что даже если это разрешено, оно не будет делать то, на что вы надеялись Object
является окончательным super
всех ссылочных типов, и все это Object
в действительности нет никаких ограничений.
В вашем конкретном примере, так как любой массив ссылочного типа является Object[]
(с помощью ковариации массива Java), поэтому его можно использовать в качестве аргумента <S super T> S[] toArray(S[] a)
(если такая граница допустима) во время компиляции, и это не помешает ArrayStoreException
во время выполнения.
То, что вы пытаетесь предложить, это:
List<Integer> integerList;
и с учетом этого гипотетического super
связано на toArray
:
<S super T> S[] toArray(S[] a) // hypothetical! currently illegal in Java
компилятор должен позволять только следующее компилировать:
integerList.toArray(new Integer[0]) // works fine!
integerList.toArray(new Number[0]) // works fine!
integerList.toArray(new Object[0]) // works fine!
и никаких других аргументов типа массива (так как Integer
только те 3 типа, как super
). То есть вы пытаетесь предотвратить компиляцию:
integerList.toArray(new String[0]) // trying to prevent this from compiling
потому что, по вашему мнению, String
это не super
из Integer
, Тем не менее, Object
это super
из Integer
и String[]
является Object[]
, так что компилятор все равно позволит выше скомпилировать, даже если гипотетически вы можете сделать <S super T>
!
Таким образом, следующее все равно будет скомпилировано (так же, как и сейчас), и ArrayStoreException
во время выполнения не может быть предотвращено какой-либо проверкой во время компиляции с использованием общих родовых границ:
integerList.toArray(new String[0]) // compiles fine!
// throws ArrayStoreException at run-time
Дженерики и массивы не смешиваются, и это одно из многих мест, где это показано.
Пример без массива
Снова, скажем, у вас есть это обобщенное объявление метода:
<T super Integer> void add(T number) // hypothetical! currently illegal in Java
И у вас есть эти объявления переменных:
Integer anInteger
Number aNumber
Object anObject
String aString
Ваше намерение с <T super Integer>
(если это законно), что это должно позволить add(anInteger)
, а также add(aNumber)
и конечно add(anObject)
, но нет add(aString)
, Что ж, String
является Object
, так add(aString)
все равно все равно скомпилируется.
Смотрите также
Смежные вопросы
По правилам набора дженериков:
- Любой простой способ объяснить, почему я не могу сделать
List<Animal> animals = new ArrayList<Dog>()
? - java дженерики (не) ковариация
- Что такое сырой тип и почему мы не должны его использовать?
- Объясняет, как сырой тип
List
отличается отList<Object>
который отличается отList<?>
- Объясняет, как сырой тип
По использованию super
а также extends
:
Java Generics: What is PECS?
- Из эффективной Java 2-й редакции: "продюсер
extends
потребительsuper
"
- Из эффективной Java 2-й редакции: "продюсер
- В чем разница между
super
а такжеextends
в Java Generics - В чем разница между
<E extends Number>
а также<Number>
? - Как я могу добавить к
List<? extends Number>
структуры данных? (ВЫ НЕ МОЖЕТЕ!)
Поскольку никто не дал удовлетворительного ответа, правильный ответ выглядит так: "Из-за недостатка языка Java".
Polygenelubricants предоставили хороший обзор плохих вещей, происходящих с ковариацией java-массива, которая сама по себе является ужасной особенностью. Рассмотрим следующий фрагмент кода:
String[] strings = new String[1];
Object[] objects = strings;
objects[0] = 0;
Этот явно неправильный код компилируется без обращения к какой-либо "супер" конструкции, поэтому ковариация массива не должна использоваться в качестве аргумента.
Теперь у меня есть совершенно правильный пример кода, требующего super
в параметре именованного типа:
class Nullable<A> {
private A value;
// Does not compile!!
public <B super A> B withDefault(B defaultValue) {
return value == null ? defaultValue : value;
}
}
Потенциально поддерживает хорошее использование:
Nullable<Integer> intOrNull = ...;
Integer i = intOrNull.withDefault(8);
Number n = intOrNull.withDefault(3.5);
Object o = intOrNull.withDefault("What's so bad about a String here?");
Последний фрагмент кода не компилируется, если я удаляю B
в общем, так B
действительно необходимо.
Обратите внимание, что возможность, которую я пытаюсь реализовать, легко получается, если я инвертирую порядок объявлений параметров типа, изменяя таким образом super
ограничение к extends
, Однако это возможно только в том случае, если я переписываю метод как статический:
// This one actually works and I use it.
public static <B, A extends B> B withDefault(Nullable<A> nullable, B defaultValue) { ... }
Дело в том, что это ограничение языка Java действительно ограничивает некоторые возможные другие полезные функции и может потребовать некрасивых обходных путей. Интересно, что будет, если нам понадобится withDefault
быть виртуальным.
Теперь, чтобы соотнести с тем, что сказали полигеномасла, мы используем B
здесь, чтобы не ограничивать тип объекта, передаваемого как defaultValue
(см. строку, использованную в примере), но скорее чтобы ограничить ожидания вызывающего абонента относительно объекта, который мы возвращаем. Как правило, вы используете extends
с типами, которые вы требуете и super
с типами, которые вы предоставляете.
"Официальный" ответ на ваш вопрос можно найти в отчете об ошибках Sun/Oracle.
ВТ2: ОЦЕНКА
Увидеть
http://lampwww.epfl.ch/~odersky/ftp/local-ti.ps
в частности, раздел 3 и последний абзац на странице 9. Допуск переменных типа по обеим сторонам ограничений подтипа может привести к набору уравнений типа без единого наилучшего решения; следовательно, вывод типа не может быть сделан с использованием любого из существующих стандартных алгоритмов. Вот почему переменные типа имеют только "расширяемые" границы.
Подстановочные знаки, с другой стороны, не должны выводиться, поэтому в этом ограничении нет необходимости.
@ ###. ### 2004-05-25
Да; Ключевым моментом является то, что подстановочные знаки, даже когда они захвачены, используются только в качестве входных данных процесса вывода; в результате ничего не следует (только) с нижней границей.
@ ###. ### 2004-05-26
Я вижу проблему. Но я не вижу, как это отличается от проблем, которые у нас возникают с нижними границами подстановочных знаков во время вывода, например:
List супер номер> s;
логическое значение b;
...
s = b? s: s;В настоящее время мы выводим List
, где X расширяет Object как тип условного выражения, что означает, что присвоение недопустимо. @ ###. ### 2004-05-26
К сожалению, разговор заканчивается там. Документ, на который ссылалась (теперь мертвая) ссылка, является Inferred Type Instantiation для GJ. От просмотра последней страницы все сводится к следующему: если допускаются нижние границы, вывод типа может дать несколько решений, ни одно из которых не является принципиальным.
Единственная причина в том, что нет смысла объявлять параметр типа с ключевым словом super при определении на уровне класса. Единственной логичной стратегией стирания типов для Java было бы вернуться к супертипу всех объектов, которым является класс Object.
Отличный пример и объяснение можно найти здесь:http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#Why%20is%20there%20no%20lower%20bound%20for%20type%20parameters?
Простой пример правил стирания типов можно найти здесь:https://www.tutorialspoint.com/java_generics/java_generics_type_erasure.htm#:~:text=Type%20erasure%20is%20a%20process,there%20is%20no %20время выполнения%20накладные расходы.
Мне очень нравится принятый ответ, но я хотел бы взглянуть на него немного по-другому.
super
поддерживается в типизированном параметре только для обеспечения возможности контравариантности. Когда дело доходит до ковариации и контравариантности, важно понимать, что Java поддерживает только вариацию по месту использования. В отличие от Kotlin или Scala, которые допускают вариацию на уровне объявления. Документация Kotlin очень хорошо объясняет это здесь. Или, если вам больше нравится Scala, вот один для вас.
По сути, это означает, что в Java вы не можете ограничить способ использования вашего класса при его объявлении в терминах PECS. Класс может как потреблять, так и производить, и некоторые из его методов могут делать это одновременно, напримерtoArray([])
, Кстати.
Теперь причина extends
разрешено в объявлениях классов и методов, потому что это больше касается полиморфизма, чем дисперсии. А полиморфизм является неотъемлемой частью Java и ООП в целом: если метод может принимать какой-либо супертип, подтип всегда можно безопасно передать ему. И если метод на сайте объявления в качестве "контракта" должен возвращать какой-то супертип, это нормально, если он вместо этого возвращает подтип в своих реализациях.
Предположим, у нас есть:
базовые классы A > B > C и D
class A{ void methodA(){} }; class B extends A{ void methodB(){} } class C extends B{ void methodC(){} } class D { void methodD(){} }
классы обертки работы
interface Job<T> { void exec(T t); } class JobOnA implements Job<A>{ @Override public void exec(A a) { a.methodA(); } } class JobOnB implements Job<B>{ @Override public void exec(B b) { b.methodB(); } } class JobOnC implements Job<C>{ @Override public void exec(C c) { c.methodC(); } } class JobOnD implements Job<D>{ @Override public void exec(D d) { d.methodD(); } }
и один класс менеджера с 4 различными подходами для выполнения работы над объектом
class Manager<T>{ final T t; Manager(T t){ this.t=t; } public void execute1(Job<T> job){ job.exec(t); } public <U> void execute2(Job<U> job){ U u= (U) t; //not safe job.exec(u); } public <U extends T> void execute3(Job<U> job){ U u= (U) t; //not safe job.exec(u); } //desired feature, not compiled for now public <U super T> void execute4(Job<U> job){ U u= (U) t; //safe job.exec(u); } }
с использованием
void usage(){ B b = new B(); Manager<B> managerB = new Manager<>(b); //TOO STRICT managerB.execute1(new JobOnA()); managerB.execute1(new JobOnB()); //compiled managerB.execute1(new JobOnC()); managerB.execute1(new JobOnD()); //TOO MUCH FREEDOM managerB.execute2(new JobOnA()); //compiled managerB.execute2(new JobOnB()); //compiled managerB.execute2(new JobOnC()); //compiled !! managerB.execute2(new JobOnD()); //compiled !! //NOT ADEQUATE RESTRICTIONS managerB.execute3(new JobOnA()); managerB.execute3(new JobOnB()); //compiled managerB.execute3(new JobOnC()); //compiled !! managerB.execute3(new JobOnD()); //SHOULD BE managerB.execute4(new JobOnA()); //compiled managerB.execute4(new JobOnB()); //compiled managerB.execute4(new JobOnC()); managerB.execute4(new JobOnD()); }
Любые предложения, как реализовать execute4 сейчас?
========== отредактировано =======
public void execute4(Job<? super T> job){
job.exec( t);
}
Спасибо всем:)
========== отредактировано ==========
private <U> void execute2(Job<U> job){
U u= (U) t; //now it's safe
job.exec(u);
}
public void execute4(Job<? super T> job){
execute2(job);
}
намного лучше, любой код с U внутри execute2
супер тип U становится именованным!
интересная дискуссия:)