Почему я могу использовать super
только с подстановочными знаками, а не с параметрами типа?
Например, в интерфейсе Collection
почему метод toArray
не написан таким образом
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
Почему я могу использовать super
только с подстановочными знаками, а не с параметрами типа?
Например, в интерфейсе Collection
почему метод toArray
не написан таким образом
interface Collection<T>{
<S super T> S[] toArray(S[] a);
}
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
of Integer
, a 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)
, но NOT add(aString)
. Ну, String
- это Object
, поэтому add(aString)
все равно будет компилироваться.
В правилах типизации generics:
List<Animal> animals = new ArrayList<Dog>()
?List
отличается от List<Object>
, который отличается от List<?>
При использовании super
и extends
:
Java Generics: What is PECS?
extends
потребитель super
" super
и extends
в Java Generics.<E extends Number>
и <Number>
?List<? extends Number>
данные? (ВЫ НЕ МОЖЕТЕ!)Поскольку никто не дал удовлетворительного ответа, правильный ответ, по-видимому, "без уважительной причины".
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 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
было виртуально.
Теперь, чтобы соотнести с тем, что сказали defaultValue
, мы используем здесь B
не для ограничения типа объекта, передаваемого как defaultValue
(см. defaultValue
использованную в примере), а для ограничения ожиданий вызывающего абонента относительно возвращаемого нами объекта. Как простое правило, вы используете extends
с типами, которые вы запрашиваете, и super
с типами, которые вы предоставляете.
"Официальный" ответ на ваш вопрос можно найти в отчете Sun/Oracle.
ВТ2: ОЦЕНКА
См
http://lampwww.epfl.ch/~odersky/ftp/local-ti.ps
в частности, раздел 3 и последний абзац на стр. 9. Принятие тип переменных с обеих сторон ограничений подтипа может привести к множество уравнений типа без единого наилучшего решения; вследствие этого, вывод типа не может быть выполнен с использованием какого-либо существующего стандарта алгоритмы. Вот почему переменные типа имеют только "расширяющие" границы.
С другой стороны, подстановочные знаки не обязательно должны быть выведены, поэтому не требуется для этого ограничения.
@###. ### 2004-05-25
Да; ключевым моментом является то, что подстановочные знаки, даже если они были захвачены, используются только как входы процесса вывода; ничего с (только) нижняя граница потребностей как результат.
@###. ### 2004-05-26
Я вижу проблему. Но я не вижу, как он отличается от проблем мы имеем с нижними границами подстановочные знаки во время вывода, например:
Список <? super Number > s;
boolean b,
...
s = b? s: s;В настоящее время мы выводим список <X> где X расширяет Object как тип условное выражение, что означает, что присвоение является незаконным.
@###. ### 2004-05-26
К сожалению, разговор заканчивается. Бумага, на которую ссылалась (теперь мертвая) ссылка, была Inferred Type Instantiation для GJ. От взгляда на последнюю страницу это сводится к: если допустимы нижние границы, вывод типа может давать несколько решений, ни одна из которых не является principal.
Мне действительно нравится принятый ответ, но я бы хотел взглянуть на него немного иначе.
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 safe
job.exec(u);
}
public void execute4(Job<? super T> job){
execute2(job);
}
намного лучше, любой код с U внутри execute2
супер тип U становится именованным!
интересное обсуждение:)