以下代码成功编译并运行,没有任何异常
import java.util.ArrayList;
class SuperSample {}
class Sample extends SuperSample {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
ArrayList<Sample> sList = new ArrayList<Sample>();
Object o = sList;
ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;
ssList.add(new SuperSample());
} catch (Exception e) {
e.printStackTrace();
}
}
}
线路ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;
不应该产生ClassCastException
吗?
虽然下面的代码会产生编译时错误来防止堆污染,但上面提到的代码在运行时不应该有类似的预防措施吗?
ArrayList<Sample> sList = new ArrayList<Sample>();
ArrayList<SuperSample> ssList = (ArrayList<SuperSample>) sList;
编辑:
如果类型擦除是这背后的原因,难道不应该有其他机制来防止无效对象添加到列表中吗?例如
String[] iArray = new String[5];
Object[] iObject = iArray;
iObject[0]= 5.5; // throws ArrayStoreException
那么为什么,
ssList.add(new SuperSample());
没有被要求抛出任何异常?
不应该,在运行时两个列表都具有相同的类型ArrayList。这叫做擦除。泛型参数不是已编译类的一部分,它们在编译过程中都会被删除。从JVM的角度来看,您的代码等于:
public static void main(String[] args) {
try {
ArrayList sList = new ArrayList();
Object o = sList;
ArrayList ssList = (ArrayList)o;
ssList.add(new SuperSample());
} catch (Exception e) {
e.printStackTrace();
}
}
基本上,泛型只是通过产生编译时错误和警告来简化开发,但它们根本不会影响执行。
编辑:
这背后的基本概念是Reifiable Type。我强烈建议阅读本手册:
可具体化类型是其类型信息完全可用的类型在运行时。这包括基元、非泛型类型、原始类型、,以及调用未绑定的通配符。
不可具体化类型是在按类型擦除的编译时间
简而言之:数组是可验证的,而泛型集合则不是。因此,当您在数组中存储smth时,JVM会检查类型,因为数组的类型在运行时存在。数组只代表一部分内存,而集合是一个普通的类,可能有任何类型的实现。例如,它可以将数据存储在数据库中,也可以存储在引擎盖下的磁盘上。若您想深入了解,我建议您阅读Java泛型和集合这本书。
在您的代码示例中,
class SuperSample { }
class Sample extends SuperSample { }
...
ArrayList<Sample> sList = new ArrayList<Sample>();
Object o = sList;
ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;
最后一行不应该生成
ClassCastException
吗?
没有。当JVM在运行时检测到不兼容的类型被强制转换时,就会抛出该异常。正如其他人所指出的,这是因为泛型类型的擦除。也就是说,泛型类型只有编译器知道。在JVM级别,变量的类型都是ArrayList
(泛型已被擦除),因此在运行时没有ClassCastException
。
顺便说一句,与其分配给Object
类型的中间局部变量,一种更简洁的分配方法是通过raw:进行强制转换
ArrayList<SuperSample> ssList = (ArrayList)sList;
其中"原始"类型是泛型类型的擦除版本。
难道不应该有其他机制来防止无效对象被添加到列表中吗?
是的,有。第一种机制是编译时检查。在您自己的答案中,您在Java语言规范中找到了正确的位置,它描述了堆污染,这是列表中出现的无效对象的术语。底部的货币报价是
如果没有发生需要发出编译时未检查警告的操作,并且具有不可具体化元素类型的数组变量没有发生不安全的别名,则不会发生堆污染。
因此,您要寻找的机制在编译器中,编译器会通过编译警告通知您这一点。但是,您已经通过使用@SuppressWarnings
注释禁用了此机制。如果要删除此注释,则会在有问题的行处收到编译器警告。如果您绝对希望防止堆污染,请不要使用@SuppressWarnings
,并将选项-Xlint:unchecked -Werror
添加到javac
命令行中。
第二种机制是运行时检查,它需要使用一个已检查的包装器。将sList
的初始化替换为以下内容:
List<Sample> sList = Collections.checkedList(new ArrayList<Sample>(), Sample.class);
这将导致在将SuperSample
添加到列表的点处抛出ClassCastException
。
这里回答您问题的关键是在java 中键入擦除
对于第一种情况,在编译时会有一个警告,而不是在第二种情况下,因为对象的间接性会阻止编译器向您发出警告(我猜在将参数化类型转换为另一种类型时会发出这个警告,而在第二个情况下没有这样做,如果有人能确认我很乐意在这里谈论它的话)。您的代码之所以运行,是因为最终sList
ssList
和o
都是ArrayList
我认为这不能产生ClassCastException,因为Java中存在向后兼容性问题。
字节码中不包含泛型信息(编译器在编译过程中会删除它)。
想象一下,您在项目中使用一些旧的遗留代码(在java1.4中编写的一些旧库),并将泛型List传递给该遗留代码中的某个方法的场景。你可以这样做。
在泛型之前,遗留代码被允许将任何东西(基元除外)放入集合中。因此,即使这个遗留代码试图将String放入List<整数>。从遗留代码的角度来看,它只是List。
因此,这种奇怪的行为是类型擦除和允许Java向后兼容性的结果。
编辑:
数组会出现ArrayStoreException,因为在运行时JVM知道数组的类型,而集合不会出现任何异常,因为类型擦除和这种向后兼容性问题JVM在运行时不知道集合的类型。
您可以在"SCJP Sun®Java认证程序员"中阅读此主题™第7章"Generics and Collections"中的"6 Study Guide"一书
参数化类型的变量可能引用的对象不是该参数化类型的。这种情况被称为堆污染。这种情况只有当程序执行了一些会产生到编译时未检查的警告。
例如,代码:
List l = new ArrayList<Number>();
List<String> ls = l; // unchecked warning
会产生未检查的警告,因为在编译时都无法确定-时间(在编译时类型检查规则的限制内)或运行时,无论变量l确实是指CCD_ 18。如果执行上面的代码,就会出现堆污染,因为变量ls被声明为List<String>
是指实际上不是List<String>
的值。无法在运行时识别问题,因为类型变量未具体化,因此实例在运行时不携带任何关于实际类型的信息用于创建它们的参数。