使用'super'关键字绑定泛型

时间:2010-05-10 04:48:54

标签: java generics language-design super

为什么我只能将super用于通配符而不用于类型参数?

例如,在Collection界面中,为什么toArray方法不是这样编写的

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

5 个答案:

答案 0 :(得分:47)

super绑定一个命名类型参数(例如<S super T>)而不是通配符(例如<? super T> ILLEGAL 只是因为即使它被允许,它不会做你希望它会做的事情,因为Object是所有引用类型的最终super,并且一切都是Object有效没有约束

在您的具体示例中,由于任何引用类型数组是Object[](通过Java数组协方差),因此它可以用作<S super T> S[] toArray(S[] a)的参数(如果这样的约束是合法的)在编译时,它不会在运行时阻止ArrayStoreException

你想要提出的建议是:

List<Integer> integerList;

并且在<{1}}上绑定假设的 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! 只有{3}类型的这3种类型。也就是说,您正试图阻止编译:

Integer

因为,根据您的论点,super不是integerList.toArray(new String[0]) // trying to prevent this from compiling 的{​​{1}}。 但是Stringsuper IntegerObjectsuper,因此编译器仍然会让上面的编译,即使假设你可以做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)仍然可以编译。


另见

相关问题

关于泛型类型规则:

使用ListList<Object>

答案 1 :(得分:29)

由于没有人提供满意的答案,正确答案似乎是“无缘无故”。

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是虚拟的,会发生什么。

现在,为了与polygenelubricants所说的相关联,我们在这里使用B不要限制作为defaultValue传递的对象的类型(参见示例中使用的字符串),而是限制调用者对我们返回的对象的期望。作为一项简单的规则,您可以将extends与您要求的类型一起使用,并将super与您提供的类型一起使用。

答案 2 :(得分:11)

您的问题的“官方”回答可以在Sun/Oracle bug report中找到。

  

BT2:评价

     

     

http://lampwww.epfl.ch/~odersky/ftp/local-ti.ps

     

特别是第3节和第9页的最后一段。承认   子类型约束两侧的类型变量可以导致a   没有单一最佳解的类型方程组;所以,   使用任何现有标准都无法进行类型推断   算法。这就是为什么类型变量只有“扩展”边界。

     另一方面,

通配符不必推断,所以那里   不需要这种约束。

     

@ ###。### 2004-05-25

     

是;关键点在于,只有使用通配符,即使在捕获时也是如此   作为推理过程的输入;没有(仅)下限需求   作为结果推断出来。

     

@ ###。### 2004-05-26

     

我看到了问题。但我不明白它与问题的区别   我们在推理过程中使用了通配符的下限,例如:

     

列表与LT ;?超级数字&gt; S;
  布尔b;
  ...
  s = b? s:s;

     

目前,我们推断List&lt; X&gt;其中X扩展Object作为类型   条件表达式,意味着赋值是非法的。

     

@ ###。### 2004-05-26

可悲的是,谈话在那里结束了。 (现在已死)链接用于指向的论文是Inferred Type Instantiation for GJ。通过浏览最后一页,它归结为:如果允许下限,则类型推断可能会产生多个解,其中没有一个是principal

答案 3 :(得分:-1)

假设我们有:

  • 基本类A&gt; 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);
    }

更好,任何代码都在exe2中使用了

超级型U被命名为!

有趣的讨论:)

答案 4 :(得分:-1)

我真的很喜欢这个被接受的答案,但是我希望对此有一个稍微不同的看法。

类型参数中支持

super只是为了允许 contravariance 功能。关于协方差 contravariance ,了解Java仅支持使用地点差异非常重要。与Kotlin或Scala不同,后者允许声明站点差异。 Kotlin文档对此进行了很好的解释here。或者,如果您更喜欢Scala,here就是您的最佳选择。

这基本上意味着在Java中,当根据PECS声明类时,您不能限制使用类的方式。该类既可以消费也可以产生,顺便说一下,它的某些方法可以同时进行,例​​如toArray([])

现在,在类和方法声明中允许使用extends的原因是,它更多地是关于多态性而不是关于变异性。而多态性通常是Java和OOP的固有部分:如果方法可以接受某些超类型,则始终可以将子类型安全地传递给它。而且,如果某个方法(在声明站点中是“合同”)应返回某个超类型,则在其实现中返回子类型代替它完全没问题