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

Ограничение дженериков с помощью ключевого слова 'super'

Почему я могу использовать super только с подстановочными знаками, а не с параметрами типа?

Например, в интерфейсе Collection почему метод toArray не написан таким образом

interface Collection<T>{
    <S super T> S[] toArray(S[] a);
}
4b9b3361

Ответ 1

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:

При использовании super и extends:

Ответ 2

Поскольку никто не дал удовлетворительного ответа, правильный ответ, по-видимому, "без уважительной причины".

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 с типами, которые вы предоставляете.

Ответ 3

"Официальный" ответ на ваш вопрос можно найти в отчете 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.

Ответ 4

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

super поддерживается в типизированном параметре только для возможности контравариантности. Когда дело доходит до ковариации и контравариации, важно понимать, что Java поддерживает только вариацию использования сайта. В отличие от Kotlin или Scala, которые допускают декларацию на сайте. Документация Kotlin объясняет это очень хорошо здесь. Или, если вам больше нравится Scala, здесь для вас.

Это в основном означает, что в Java вы не можете ограничивать использование своего класса, когда объявляете его в терминах PECS. Класс может и потреблять, и производить, и некоторые из его методов могут делать это одновременно, например, toArray([]).

Теперь причина, по которой extends допускается в объявлениях классов и методов, заключается в том, что в нем больше говорится о полиморфизме, чем о дисперсии. И полиморфизм является неотъемлемой частью Java и ООП в целом: если метод может принимать некоторый супертип, ему всегда можно безопасно передать подтип. И если метод на сайте объявления как "контракт" должен возвращать некоторый супертип, то вполне нормально, если он возвращает подтип вместо этого в своих реализациях

Ответ 5

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

  • базовые классы 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 становится именованным!

интересное обсуждение:)