我正在阅读一篇关于 Java 协方差的有趣的 dzone 文章,该文章很容易理解,但有件事困扰着我,这没有意义,这篇文章在这里 https://dzone.com/articles/covariance-and -逆变
我在这里引用文章中的示例,其中解释了为什么无法将集合添加到:
通过协方差,我们可以从结构中读取项目,但我们不能向其中写入任何内容。所有这些都是有效的协变声明。
List<? extends Number> myNums = new ArrayList<Integer>();
因为我们可以确定无论实际列表包含什么,它都可以向上转换为 Number (毕竟任何扩展 Number 的东西都是 Number,对吧?) 但是,我们不允许将任何内容放入协变结构中。
myNums.add(45); //compiler error
这是不允许的,因为编译器无法确定泛型结构中对象的实际类型是什么。它可以是任何扩展 Number 的内容(例如 Integer、Double、Long),但编译器无法确定是什么
上面的段落对我来说没有意义,编译器知道列表包含
Number
或扩展它的任何内容,并且 ArrayList
被输入为 Integer。并且编译器知道插入的文字 int。
那么为什么它要强制执行此操作,因为在我看来它可以确定类型?
你遗漏了两点:
您仅考虑局部变量:
public void myMethod() {
List<? extends Number> list = new ArrayList<Integer>();
list.add(25);
}
编译器可以轻松检测到
?
的实际值,但据我所知没有人会编写这样的代码;如果要使用整数列表,只需将变量声明为 List<Integer>
。
协变和逆变在处理参数和/或结果时最有用;这个例子更现实:
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>
的实例,稍后代码也可以使用具有不同参数的方法,而无需重新编译该类。上面的段落对我来说没有意义,编译器知道列表包含 Number 或扩展它的任何内容,并且 ArrayList 的类型为 Integer。并且编译器知道插入的文字 int。
不,编译器知道的是列表包含扩展 Number
(包括
Number
)的 something。它无法判断它是
List<Number>
(在这种情况下,您可以插入 Number
的任何实例)还是 List<Integer>
(在这种情况下,您只能插入 Integer
实例)。但它确实知道您使用 get
检索的所有内容都将是 Number
的实例(即使它不确定特定类)。当编译器遇到:
List<? extends Number> myNums = new ArrayList<Integer>();
它检查左侧和右侧的类型是否匹配在一起。 但编译器确实不“记住”用于右侧实例化的特定类型。
有人可以说编译器记住了这一点
myNums
:
List<? extends Number>
是的,编译器可以进行常量折叠;和数据流分析;并且编译器也有可能跟踪“实例化类型”信息 - 但可惜:java 编译器不会这样做。
它只知道 myNums
List<? extends Number>
- 仅此而已。
考虑一个略有不同但相关的示例:
Object obj = "";
通过上面的参数,编译器还应该能够知道
obj
实际上是 String
,因此您可以在其上调用
String
特定的方法:
obj.substring(0);
任何有一点 Java 经验的人都知道你做不到。你
(并且只有你)(或者至少是编写代码的人)拥有类型信息,并且已故意决定丢弃它:没有理由声明变量类型是 Object
,那么为什么编译器必须付出努力来尝试恢复该信息呢? (*)
同样的道理,如果你想让编译器知道变量的值
List<? extends Number> myNums = new ArrayList<Integer>();
是一个
ArrayList<Integer>
,将变量声明为该类型。否则,编译器假设“这绝对可以是类型范围内的任何内容”就简单得多。
(*)我在另一个答案中的某个地方读到了这个论点。我不记得它在哪里,否则我会给出适当的归属。