为什么边界在 Java 中工作如此奇怪?

Why bounds work so strange in Java?

我正在使用 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());

感谢您的回答!

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

对于问题 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> 的匿名 class,并有一个调用 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或其超class、A。如果它是 List<B> 怎么办?您不能将 A 添加到 List<B>;因此编译器拒绝此代码。

简短的回答 ("why it is strange") 是某些 Java 简写符号使两段代码看起来非常相似,但实际上它们非常不同。

所以它看起来像 "if this works, this should also work",但事实并非如此,因为两位代码的重要差异被掩盖了。

我们分别来看几段代码:

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

所以有人会给我们一个请求 List<?> 的方法。我们可以这样使用它:

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

但如果我们这样做

list1.add(new A());

编译器无法证明这是合法的,因为这取决于我们是否碰巧获得了 returns 一个 List<A>List<String> 的 ListFactory 实现。

如果我们把上面的替换成

会怎样
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()); 是 shorthand。在 SpecialList() 构造函数中,this 是 SpecialList 类型的引用 - 声明为扩展 ArrayList<A> - 当然我们可以 add(new A())ArrayList<A> 的子类。

所以现在我们拥有制作类似于您的原始代码的所有部分:

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

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

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