【问题标题】:Combining <? extends ClassE> and <? super ClassB>结合 <?扩展 ClassE> 和 <?超级B类>
【发布时间】:2017-07-23 13:53:55
【问题描述】:

我怎么理解?延伸..和? super .. 自己工作,哪些泛型类型是可能的,但我就是不明白这个层次结构如何实现以下内容:

-> 表示扩展

等级为 X(最低),A 到 E(最高)

接口是F

X -> A(实现 F)-> B -> C -> E(实现 F)

还有 D -> E

public class Node<T extends ClassE> {
    private T info;

    public T getInfo() {
        return info;
    }

    public void setInfo(T info) {
        this.info = info;
    }
}

public static void main (String [] args){

    Node<? super ClassB> n2 = new Node<ClassC>(); 
// this makes sense, since Node accepts below E and above B

    InterfaceF i2 = n2.getInfo();
// how? Not only outside of <? extends E> but also getting value even though 
// <? super B> is defined above, what's up with PECS?

    n2.setInfo(new ClassX());
// also.. how? I'm setting a class that's out of the allowed range + 
// seemingly violating the PECS for <? extends E>
}

正如您所看到的,在组合它们时我完全感到困惑,而且让这些声明毫无问题地通过编译器让我感到非常惊讶。 我在某处读到,在 Java 中不可能同时使用这两种界限,但那它是如何工作的呢?

【问题讨论】:

  • 你确定这个Node&lt;? super ClassB&gt; n2 = new Node&lt;ClassC&gt;(); 编译了吗?参考 n2 需要 Node 参数类型为 B 或更高,而您试图传递低于继承层次结构中 B 的 C。
  • 我没有分配任何值来测试它,但我完全能够执行程序,我 100% 确定
  • 关于层次结构,E是最高的,X是最低的,很抱歉造成混淆。所以A扩展B,B扩展C,C扩展E
  • PECS 只是一个内存设备,实际的 Java 语言规则是抽象的,不涉及生产者/消费者关系等具体模式。

标签: java generics extends super


【解决方案1】:

第一行InterfaceF i2 = n2.getInfo(); 编译,因为下界通配符仍然保留类型变量本身的界限。由于类型变量有一个上限ClassEgetInfo() 仍然返回一个ClassE。由于ClassE 实现了InterfaceF,因此赋值编译。

换句话说,我们可以想象,当您执行Node&lt;? super ClassB&gt; 时,您实际上隐含地执行了类似(虚构语法)Node&lt;? extends ClassE &amp; super ClassB&gt; 的操作。 Node 的类型参数既是ClassB 的超类型,又是ClassE 的子类型。

这类似于Node&lt;?&gt; 隐含地与Node&lt;? extends ClassE&gt; 相同。

实际指定的方式有点复杂,但它在capture conversion 中。捕获转换是编译器获取带有通配符的类型并将其视为没有通配符的类型的过程,目的是确定子类型。

Gn 类型参数A<sub>1</sub>,...,A<sub>n</sub> 和对应的边界U<sub>1</sub>,...,U<sub>n</sub> 命名一个泛型类型声明。

存在从参数化类型G&lt;T<sub>1</sub>,...,T<sub>n</sub>&gt; 到参数化类型G&lt;S<sub>1</sub>,...,S<sub>n</sub>捕获转换,其中,对于1 ≤ i ≤ n

  • [...]

  • 如果T<sub>i</sub>? super B<sub>i</sub> 形式的通配符类型参数,则S<sub>i</sub> 是一个新类型变量,其上限为U<sub>i</sub>[A<sub>1</sub>:=S<sub>1</sub>,...,A<sub>n</sub>:=S<sub>n</sub>],下限为B<sub>i</sub>

换言之,S<sub>i</sub>(捕获转换后对应? super ClassB的类型参数)从通配符的边界获取其下限,从类型变量声明的边界获取其上限。

第二行n2.setInfo(new ClassX()); 可以编译,因为ClassXClassB 的子类,所以它可以隐式转换为它。我们可以想象n2Node&lt;ClassB&gt;,这行编译的原因可能更明显:

Node<ClassB> n2 = ...;
n2.setInfo(new ClassX());

setInfo 接受ClassB 以及ClassB 的任何子类型。

另外,关于这个:

我在某处读到,Java 中不可能同时使用这两个边界,但那是如何工作的呢?

编译器中的类型系统做了很多我们自己无法明确做的事情。另一个很好的例子(虽然不相关)是匿名类的类型推断:

int num = Objects.requireNonNull(new Object() {int num = 42;}).num;
System.out.println(num); // 42

它编译是因为允许类型推断推断requireNonNullT 的类型参数是匿名对象类型,即使我们自己永远无法将该类型作为显式类型参数提供。

【讨论】:

  • 感谢您抽出宝贵时间深入解释。我做了一些测试,问题是我没有看方法的类型。由于读取方法(如 getInfo)需要我的类型 T 的上界,因此 n2.getInfo() 只能是类型 E 或更低。因此我可以采取任何上述对象 i2 = n2.getInfo();它对于 setInfo() 的工作原理类似,这是一个我需要取下限的写入操作。另外,“匿名对象类型”是什么意思?你的意思是像 Object$1 这样的匿名类有自己的类型吗?
  • “你的意思是像 Object$1 这样的匿名类有自己的类型吗?” 是的,当你写一个像 new Object() {} 这样的匿名对象时,你是在隐式声明一个新的类,但匿名类没有名称,因此无法引用它。在我的示例中,除了类型推断正在执行的操作之外,无法访问匿名对象之外的num。 (我的例子不是任何人都应该使用的代码。这只是一个奇怪的编译器怪癖的例子。)
  • 好吧,我明白了。我认为可能会有一些我没有看到的好处(除了来自匿名课程的明显好处)。非常有趣的例子。
【解决方案2】:

免责声明:我不知道具体的类型推断规则,但请解释为我的最佳理解。

关于InterfaceF i2 = n2.getInfo() - 因为Node&lt;T extends E&gt; 保证Node.getInfo() 返回一些extends E。根据您的描述E implements F。这样可以确保T extends E 也将implement F。因此Node.getInfo() = T extends E implements F。所以n2.getInfo() implements F 很好。

关于n2.setInfo(new ClassX()) - 我没有像上面那样正式的解释,但让我们试着考虑一下:基本上你Node&lt;? super ClassB&gt;告诉大家期待 最多 ClassBNode 内容的最低值。然而,由于ClassX 传递继承ClassB,它完全有效,因为它将满足? super ClassB 提出的所有接口保证。

希望这会有所帮助!

【讨论】:

  • 这是我在另一个示例中尝试的方法,非常棒。它特别有用,因为它就像一步一步的一样,也很容易应用于其他场景。
【解决方案3】:
public class Test {
    public interface F {}
    public static class E implements F {}
    public static class D extends E {}
    public static class C extends E {}
    public static class B extends C {}
    public static class A extends B implements F {}
    public static class X extends A {}

    public static class Node<T extends E> {
        private T info;
        public T getInfo() {return info;}
        public void setInfo(T info) {this.info = info;}
    }

    public static void main(String[] args) {
        Node<? super B> n = new Node<C>(); // C is superclass of B = OK
        F i = n.getInfo(); // node type = B|C|E all these types implements F (since E implements F) = OK
        n.setInfo(new X()); // X has supertypes A,B,C,E = can be casted to B and so satisfy <? super B>
    }
}

【讨论】:

  • 你能解释一下你做了什么或改变了什么吗?解释通常比仅提供代码的答案更有帮助。
  • @Zabuza main 方法上面的所有代码都遵循 OP 的继承,main 方法有 cmets 很好地解释了发生了什么,那么为什么非代码行?
  • 可靠的答案,直截了当,我花了一些时间才弄清楚每个 get - 操作都需要和上限本身,每个 set 操作都需要一个下限。结合您的解释,我想一切都变得清晰了。
猜你喜欢
  • 2012-09-18
  • 2011-05-14
  • 1970-01-01
  • 2016-10-21
  • 2010-12-26
  • 1970-01-01
  • 2012-10-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多