4

我正在使用 Java 8。在通过 Java OCP 8 的培训期间,我发现了一些我不理解并想知道的代码片段,为什么它对我来说如此奇怪。

我有下一个层次结构:

class A {}
class B extends A {}
class C extends B {}

一个,此代码有效:

List<?> list1 = new ArrayList<A>() { 
    { 
        add(new A());
    }
};

但是下一个代码不起作用,编译错误:

list1.add(new A());

那么,为什么我们不能以这种方式添加新记录呢?

第二,此代码有效:

List<? extends A> list2 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    } 
};

但是下一个代码不起作用,编译错误:

list2.add(new A());
list2.add(new B());
list2.add(new C());

最后一个,这段代码是有效的:

List<? super B> list3 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    }
};

但是在接下来的代码中,当我们添加new A()时,编译错误:

list3.add(new A()); // compilation error
list3.add(new B());
list3.add(new C());

感谢您的回答!

4

2 回答 2

4

这是一个旨在强制类型安全的编译错误。如果编译器允许你这样做,想象一下会发生什么:

对于问题 1,一旦list1声明了对象,编译器只考虑声明的类型,List<?>而忽略它最近分配给ArrayList<A>.

List<?> list1 = ...;  // The compiler knows list1 is a list of a certain type
                      // but it's not specified what the type is. It could be
                      // a List<String> or List<Integer> or List<Anything>
list1.add(new A());   // What if list1 was e.g. a List<String>?

但:

List<?> list1 = new ArrayList<A>() { 
    { 
        add(new A());
    }
};

在这里,您正在分配list1一个表达式。表达式本身,即 之后的所有内容=,不使用?,实际上是一个匿名类,它扩展ArrayList<A>并具有一个可以调用的初始化程序块add(new A())

第二个问题(与 list2)具有相同的原因。

在第三期,

List<? super B> list3 = new ArrayList<A>() {
    {
        add(new A());
        add(new B());
        add(new C());
    }
};

list3.add(new A()); // compilation error

编译器将list3其视为List<? super B>. 这意味着泛型参数可以是B或其超类,A. 如果它是一个List<B>怎么办?您不能将 a 添加A到 a List<B>; 因此编译器拒绝此代码。

于 2016-10-21T12:44:18.637 回答
1

简短的回答(“为什么它很奇怪”)是某些 Java 速记符号使两段代码看起来非常相似,而它们实际上非常不同。

所以它看起来像“如果这有效,这也应该有效”,但事实并非如此,因为这两位代码的重要差异被掩盖了。

我们分别看几段代码:

public interface ListFactory {
    List<?> getList();
}

所以有人会给我们一种请求List<?>. 我们可以这样使用它:

List<?> list1 = myListFactory.getList();

但如果我们这样做

list1.add(new A());

编译器无法证明这是合法的,因为这取决于我们是否碰巧获得了返回 aList<A>或a 的 ListFactory 实现List<String>

如果我们将上面的内容替换为

List<?> list1 = new SpecialList();
list1.add(new A());

这更接近您的原始代码;但是对于编译器来说,存在同样的问题:当它评估时,list1.add(new A());它不会在 list1 的赋值历史中寻找线索。它只知道 list1 的编译时引用类型是List<?>,所以如果list1.add(new A());之前是非法的,那么这里仍然是非法的。

(乍一看,这可能感觉像是编译器的一个缺点,但我认为不是;编译器尝试知道比引用类型直接告诉它的更多信息通常是不切实际或不希望的。如果我们想要以允许add(new A())我们使用不同的引用类型的方式引用我们的对象 - 例如List<A> list2 = new SpecialList();。)

以上暗示我们有一个SpecialList.java;让我们看一下:

public class SpecialList extends ArrayList<A> {
    public SpecialList() {
        add(new A());
    }
}

这可能看起来有点傻,但就编译器而言,它没有任何问题(只要定义了 A 并导入了 java.util.ArrayList )可能也就不足为奇了。

请注意,add(new A());在这种情况下是this.add(new A());. 在 SpecialList() 构造函数中,this是 SpecialList 类型的引用——它被声明为扩展ArrayList<A>——当然我们可以add(new A())指向ArrayList<A>.

所以现在我们有所有的东西来制作类似于你的原始代码的东西:

List<?> list1 = new ArrayList<A>() {
    {
        add(new A());
    }
}
list1.add(new A());

由于我们应用了 Java 语法糖,这里的第 3 行和第 6 行现在看起来非常相似。但是第 3 行真的很像我们的 SpecialList() 示例——调用 add() 的引用类型是ArrayList<A>. 不过,第 6 行几乎就是它看起来的样子——因此它失败的原因与前几个示例中的相同。

类似的分析将解释您看到的其他奇怪的区别。

于 2016-10-21T15:51:46.947 回答