【问题标题】:Bounding generics with 'super' keyword使用“super”关键字绑定泛型
【发布时间】:2010-05-10 04:48:54
【问题描述】:

为什么super 只能与通配符一起使用,而不能与类型参数一起使用?

比如Collection接口中,为什么toArray方法不是这样写的

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

【问题讨论】:

  • 你能解释一下你在语义上打算成为 和 。在我看来,这只是语法问题。
  • 你不能? super T[] toArray(? super T[] a),可以吗?
  • 这就是为什么 Collection 类中有一个Object[] toArray() 方法。

标签: java generics language-design super


【解决方案1】:

super 绑定命名类型参数(例如&lt;S super T&gt;)而不是通配符(例如&lt;? super T&gt;)是非法,因为即使它被允许,它也不会这样做你希望它会做什么,因为Object 是所有引用类型中的终极super,并且一切都是Object实际上没有限制

在您的具体示例中,由于引用类型的 any 数组是 Object[](通过 Java 数组协方差),因此它可以用作 &lt;S super T&gt; 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 不是Integersuper然而ObjectIntegersuperString[]Object[],所以编译器仍然会让上面的代码编译,即使假设你可以做到&lt;S super T&gt;

所以下面的仍然可以编译(就像它们现在的方式一样),并且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

您对&lt;T super Integer&gt;(如果合法)的意图是它应该允许add(anInteger)add(aNumber),当然还有add(anObject),但不允许add(aString)。好吧,String 是一个Object,所以add(aString) 仍然可以编译。


另见

相关问题

关于泛型输入规则:

关于使用superextends

【讨论】:

  • 那么为什么 List addToList(List list, T element){ list.add(element);返回列表; } 没有意义?
  • “它不会像你希望的那样”——这完全是错误的。 OP 提供了一个很好的用例(以可怕的数组协方差为模),具有明显的语义:集合应该能够填充任何更通用类型的数组并返回所述数组。它没有任何问题。
  • "由于任何引用类型的数组都是Object[],因此它可以用作&lt;S super T&gt; S[] toArray(S[] a) 的参数" True。但是,如果S 被推断为Object,那么这也会将方法的返回类型限制为Object[],这可能会导致编译错误,具体取决于进行调用的上下文(如果它期望S[])。因此,将类型变量更改为 Object 并不能替代 super 绑定。
  • 有一些有效的下限用例,虽然很少。
  • polygenelubricants:这个答案的要点,即上限没有用,是不正确的。这在Rotsor的回答中得到了证明。我想鼓励您编辑此答案以指出这一点,因为它目前的形式会误导人们!
【解决方案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'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 用于您提供的类型。

【讨论】:

  • +1 您的示例与 Guava 的 Optional.or(T) 中的实际用例相匹配。来自文档:“签名public T or(T defaultValue) 过于严格。但是,理想的签名public &lt;S super T&gt; S or(S) 不是合法的Java。因此,一些涉及子类型的合理操作是编译错误”。
  • @PaulBellora ......现在 Java 有自己的 Optional 或 Stream API,它正在咬每个人。那么你能做的最好的事情就是一个实际上已经过时的.&lt;SuperType&gt;map(t -&gt; t).map(Function.&lt;SuperType&gt;identity())...
  • @Holger 我实际上记得番石榴文档中的这个定义和解释。我仍然不明白为什么要返回一个超类型;对我来说,它只是违背了目的,返回一个子类型非常有意义,我想我在这里遗漏了一些东西
  • @Eugene 这个答案的例子应该足够解释;只需将Nullable 替换为Optional 并将withDefault 替换为orElse。另一个例子是CompletableFuture&lt;CharBuffer&gt; f = someIoOperation(); CharSequence result = f.exceptionally(t -&gt; "constant fallback").join();,这是一个合理的操作,但不起作用,除非你插入像.thenApply(Function.&lt;CharSequence&gt;identity()) 这样的解决方法。另一个间接示例是 String.join(", ", () -&gt; Stream.of("foo", "bar").iterator());,它不起作用。
  • @Rotsor,您能否解释一下为什么您在帖子末尾提出的有效方法变体只能写为static?我的意思是这个声明:public static &lt;B, A extends B&gt; B withDefault(Nullable&lt;A&gt; nullable, B defaultValue) { ... }.
【解决方案3】:

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

BT2:评估

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

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

另一方面,通配符不必被推断出来,所以有 不需要这个约束。

@###.### 2004-05-25

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

@###.### 2004-05-26

我看到了问题。但我看不出它与问题有何不同 我们在推理期间对通配符有下限,例如:

列表s;
布尔 b;
...
s = b ?小号:小号;

目前,我们推断 List 其中 X 扩展 Object 作为 条件表达式,表示赋值非法。

@###.### 2004-05-26

遗憾的是,对话到此结束。 (现已失效的)链接指向的论文是Inferred Type Instantiation for GJ。看了最后一页,归结为:如果允许下限,类型推断可能会产生多种解决方案,但都不是principal

【讨论】:

  • 被低估的答案,应该被接受。方法类型参数的下限很有用(正如 Rotsor 所展示的那样)并且可以实现(正如该论文的作者在 Scala 中这样做时所展示的那样)。它们难以明智地实现显然是它们在 Java 中不允许使用的真正原因。
【解决方案4】:

假设我们有:

  • 基本类 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 被命名!

有趣的讨论:)

【讨论】:

    【解决方案5】:

    我真的很喜欢这个公认的答案,但我想对它提出一个稍微不同的看法。

    super 在类型化参数中受支持,仅允许 逆变 功能。谈到协变逆变,了解Java 仅支持use-site variance很重要。与 Kotlin 或 Scala 不同,它们允许 声明站点差异。 Kotlin 文档很好地解释了它here。或者,如果您更喜欢 Scala,here 适合您。

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

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

    【讨论】:

    • 接受的答案是错误的。这个问题是关于类型界限而不是关于方差的,这是正确的。但是使用 T super R 的下限类型有时会很有用,正如 Rotsors 的回答所证明的那样,以及对 Guava Optional 类的引用。
    猜你喜欢
    • 2021-06-09
    • 1970-01-01
    • 1970-01-01
    • 2018-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多