这是Java的工作原理,因为这就是泛型的意思,正如您已经了解的一样(方差的奇怪性质意味着允许List<Animal> = listOfDogs;
让你向你的狗列表中添加猫那是不好的 - 这就是泛型不变的要点),而且至关重要的是,因为泛型是javac
想象出来的东西!
Java没有使用泛型。在Java 1.5之前,泛型根本不是一种东西。我们只是这样编写:
void adoptDog(List pets) { ... }
即将要处理的是,pets
被视为动物列表的概念是基于文档和上下文线索中的名称来确定的,并通过“跟随链接” - 即意识到 Dog
被定义为 extends Animal
,因此可以假设将几只狗收养到新建的列表中意味着任何假定 pets
仅包含 Animal
实例的代码都没问题。
Java1.5 引入了泛型,但泛型是完全由 javac 控制的 - 它只是使其官方化:编译器检查的文档。字面上:JVM 规范(JVM Specification,即 java.exe
)不知道泛型是什么。Javac 消除了大部分内容;它进入类文件的少数地方是 java.exe 完全忽略的。它按照规范应该对它们进行的操作:它知道如何读取它们(否则它甚至无法理解类文件),但它直接跳过这些内容。
当你写下这个:
class Foo<T extends Bar> {
public <Z super Foo> void foo() {}
}
泛型确实会出现在类文件中,但唯一的原因是因为javac
可以基于源文件和类文件混合运行,并且需要知道这些类文件中的泛型才能发挥其作用。
你应该尝试玩弄一下以下代码:
Javac为您注入转换,而java.exe不关心损坏
您无法使Java编译此代码:
void foo(String str) {
int foo = str;
}
作为例子,尝试以某种方式分配内存位置/压缩ref,
str
在JVM级别下或底层直接访问它作为一个
int
(或者在64位架构上的
long
)。虽然您可以尝试操作类文件并编辑字节码来完成此操作:
NB: Not real bytecode, just serves to explain the idea
PUSH // push a string ref from the constant pool
POPI 1 // pop the top of the stack into an int-typed local var slot
但是,如果你尝试这样做,一旦类文件被加载,你会得到一个ClassVerifierError
。JVM实际上需要一些努力(并且JVM规范要求它这样做)来确保这样的花招不在其中。如果有,整个类文件本身就会被直接拒绝。
相比之下,如果你尝试使用泛型进行精确的相同戏法,java.exe根本不在乎,并且会很愉快地运行它。这是因为javac
-它自己- 这样做了。在运行时没有泛型。
void foo(List<String> list) {
return list.get(0).toLowerCase();
编译成完全相同的字节码为:
void foo(List list) {
return ((String) list.get(0)).toLowerCase();
试试吧!运行javap -c -v CompiledClassFile
来查看字节码。
这两者之间在字节码级别上唯一的区别是一个微小的变化:参数类型为List<String>
被存储在类文件中,而JVM并不知道这意味着什么。但是javac
知道,并且如果javac尝试编译调用此foo
方法的某些代码,则会考虑到它。
那么为什么这完全没有问题,而将字符串引用“pop”到int变量中的尝试如此严重,以至于JVM花费时间扫描类字节码,找到它,并拒绝整个文件?
因为javac
——注入了——那个强制转换,而强制转换是JVM本身(java.exe
)知道的东西,严格应用,因此可以防止核心转储或其他严重的堆损坏;如果你通过某种方式调用包含非字符串的列表的方法,你只会得到一个ClassCastException
。让我们试试吧!
List list = new ArrayList();
list.add(Integer.valueOf(5));
foo(list);
上面的代码将抛出ClassCastException异常。奇怪的是,它是在一个根本不包含任何转换的行上抛出的。这是因为javac插入了它。javac
会编译它,而java.exe
的验证器也完全没有问题。
Javac还应用了一个检查,纯粹作为编译器的一步;如果您尝试调用foo
方法并传递一个List<Integer>
,例如,javac
本身将拒绝编译并发出错误。但仅此而已 - javac只是拒绝了。它可以生成JVM将接受和运行的合法字节码。如果列表为空,您永远不会知道它已损坏。如果它不为空,那么就会发生ClassCastException,这不是安全或堆栈损坏问题。异常是“安全”的,它们不会导致核心转储、内存损坏、潜在的缓冲区溢出等问题。如果您修补javac以不抱怨,则生成的字节码将很好(如果调用它,则可能会引发异常,但这没关系)。
编译器允许在存在安全代码的情况下进行转换。就泛型而言,是正确的,但并不能解释你所见的现象。这只是为了方便你们而已。何必让你写毫无意义的代码呢?那只会让你写出错误的程序。
对于`(List someListOfListsOfAnimals)//编译错误`,是个怪异的问题,不是吗?你总是可以强制执行。这个代码可以编译:
List<List<Animal>> a = new ArrayList<>();
List temp = a;
List<List<T>> b = (List<List<T>>) temp;
我们甚至可以将这些转换组合成一个怪物:
List<List<T>> a = (List<List<T>>) listOfListsOfAnimals
List<List<T>> a = (List<List<T>>) (List) listOfListsOfAnimals
那第二行,看起来很愚蠢,实际上“有效”(编译;它仍然是一个警告;每当你告诉javac关于泛型的事情时,你都会得到一个警告,因为javac是这些东西的第一道防线和最后一道防线;如果javac被告知不关心它,没有任何人会关心它,因此,javac告诉你:好吧,伙计,你自己处理吧,在这里,一个警告可以确保你完全理解这些内容的承诺,这是唯一防止一些非常奇怪的错误出现在你的代码库中的东西!)。
那么,为什么?主要是“因为规范这样说”。 “为什么规范这样说”?因为……它就是这样,在某个时候你会问:当他们写下这些内容时,规范的作者在想什么,这是无法回答的,除非可能是作者本人,但据我所知,他们不经常访问Stack Overflow。
一个人可能会猜测。使用“raw”可以,因为“raw”模式允许任何内容,这正是“raw”模式的目的。仅看泛型中的内容,你正在尝试将
List<Animal>
强制转换为
List<T>
,这些类型根本没有关系(因为泛型是不变的,因此它们与彼此不同,就像
Integer
和
String
一样 - 两者都不是对方的超类型)。规范简单地说,不允许将同级别(不相关的类型)强制转换为彼此。因此,对于指定此事项的同一行规范:
Integer i = ...
String s = (Integer) i
这应该被视为编译器错误,因为它是无意义的,现在你的强制类型转换也被标记为直接错误,尽管在这里不太清楚,因为即使类型名义上完全不相关,T extends Animal
定义为T的List<List<T>>
和List<List<Animal>>
感觉非常奇怪,因此不能像这样将一个转换为另一个。
List<Animal>
与T
不兼容,因为T
扩展了 Animal,而不是 Animal 的列表。我可以理解你为什么对这段代码的目的进行了概括,因为似乎没有人会有意地编写这样的代码。 - Robert Harveyjavac
错误(即,你的怀疑基本上是正确的,两种情况都应该是警告)。 - HTNW(List<T>)listOfAnimals
是允许的,正是因为(请参见我的JLS链接)可能会出现T = Animal
的情况,因此这种转换不像(List<Dog>)listOfAnimals
那样“愚蠢”(总是错误)。 (从List<T>
到List<Animal>
存在缩小的引用转换,因为(各种超类型的)这些类型不能被证明是不同的,这是因为T
与Animal
不能被证明是不同的。)所以问题是,为什么相同的推理不允许(List<List<T>>)listsOfAnimals
,因为在T = Animal
的情况下,这种转换也不是错误的? - HTNW