Java 中的协方差 - 无法将集合添加到

Covariance in Java - Collection cannot be added to

我在 java 读了一篇关于协方差的有趣的 dzone 文章,这篇文章很容易理解,但有一件事困扰我,它没有意义,文章在这里 https://dzone.com/articles/covariance-and-contravariance

我在这里引用文章中的示例,其中解释了为什么无法将集合添加到:

有了协方差,我们可以从结构中读取项目,但我们不能向其中写入任何内容。所有这些都是有效的协变声明。

List<? extends Number> myNums = new ArrayList<Integer>();

因为我们可以确定无论实际列表包含什么,它都可以向上转换为数字(毕竟任何扩展数字的东西都是数字,对吧?) 但是,我们不允许将任何东西放入协变结构中。

myNumst.add(45); //compiler error

这是不允许的,因为编译器无法确定泛型结构中对象的实际类型。它可以是扩展 Number 的任何东西(如 Integer、Double、Long),但编译器无法确定是什么

上面的段落对我来说没有意义,编译器知道列表包含 Number 或任何扩展它的内容,并且 ArrayList 被键入为 Integer。并且编译器知道插入的文字 int。

为什么它强制执行此操作,因为在我看来它可以确定类型?

当编译器遇到:

List<? extendsNumber> myNums = new ArrayList<Integer>();

它检查左侧和右侧的类型是否匹配。 但是编译器 not "remember" 用于右侧实例化的特定类型。

可以说编译器记得关于 myNums:

  • 已初始化
  • 类型为List<? extendsNumber>

是的,编译器可以进行常量折叠;和数据流分析;编译器 可能 也可以跟踪该 "instantiation type" 信息 - 但遗憾的是:java 编译器不会那样做。

它只知道 myNums 是一个初始化的 List<? extends Numbers> - 仅此而已。

你错过了两点:

  • 您只考虑局部变量:

    public void myMethod() {
       List<? extends Number> list = new ArrayList<Integer>();
       list.add(25);
    }
    

    编译器可以很容易地检测到 ? 的实际值,但我知道没有人会写这样的代码;如果要使用整数列表,只需将变量声明为 List<Integer>.

    协变和逆变在处理参数and/or结果时最有用;这个例子比较现实:

    public List<Integer> convert(List<? extends Number> source) {
       List<Integer> target = new ArrayList<>();
       for (Number number : source) {
          target.add(number.intValue());
       }
       return target;
    }
    

    编译器如何知道用于参数化列表的类型是什么?即使在编译时所有调用仅传递 ArrayList<Integer> 的实例,稍后代码也可以使用具有不同参数的方法而无需重新编译 class。

  • The paragraph above is what doesn't make sense to me, the compiler knows that the list contains Number or anything that extends it, and the ArrayList is typed to Integer. And the compiler knows about the literal int that is inserted.

    不,编译器知道的是列表包含 扩展 Number(包括 Number)的东西。它无法判断它是 List<Number>(在这种情况下您可以插入 Number 的任何实例)还是 List<Integer>(在这种情况下您只能插入 Integer 个实例) .但它确实知道您使用 get 检索的所有内容都将是 Number 的实例(即使它不确定具体的 class)。

The paragraph above is what doesn't make sense to me, the compiler knows that the list contains Number or anything that extends it, and the ArrayList is typed to Integer. And the compiler knows about the literal int that is inserted.

考虑一个略有不同但相关的示例:

Object obj = "";

根据上面的论点,编译器也应该能够知道 obj 实际上是一个 String,因此您可以调用 String 特定的方法在上面:

obj.substring(0);

任何有一点 Java 经验的人都知道你做不到。

(和只有你)(或者,至少,写代码的人)有类型信息,并且已经做出了故意丢弃它的决定:没有理由将变量类型声明为 Object,那么为什么编译器必须投入工作来尝试恢复该信息? (*)

同理,如果想让编译器知道变量的值

List<? extends Number> myNums = new ArrayList<Integer>();

是一个 ArrayList<Integer>,将变量声明为该类型。否则,编译器假设 "this can be absolutely anything within the type bounds".

就简单多了

(*) 我在 SO 上某处的另一个答案中的某个地方读到了这个论点。我不记得它在哪里,或者我会给出适当的归属。