我在玩java泛型时,遇到了这段代码,我很困惑为什么会这样。
我将第二个参数K
作为Integer
传递,在泛型方法中,我将float
强制转换为K
类型,在main()
中,我接收它作为Integer
,
在我的代码检查器中,我看到Float
数字完全位于我的列表中(在转换为Integer
后没有被截断),它是Integer
类型的,但当我尝试拾取元素以将其保存在Integer
变量中时,它给出了ClassCastException
。
有人能解释泛型出了什么问题吗?这样就不会使我们免于抛出异常。
注意:当我从签名中删除我的第二个参数K
时,我到达了这个场景,所以不会有任何定义K
类型的东西,在这种情况下,我认为Java将其设为Object
,然后我们可能会得到强制转换异常,但为什么在这种情况中,当我传递K
类型时也是如此。
import java.util.ArrayList;
import java.util.List;
public class IntegerPrinter {
Integer item;
public void print() {
System.out.println(item);
}
public <T,K> List<K> anyPrint(List<T> num,K lo) {
List<K> mylist = new ArrayList<>();
mylist.add( (K) new Float(2.99f));
return mylist;
}
public IntegerPrinter(Integer item) {
this.item = item;
}
}
import java.util.ArrayList;
import java.util.List;
public class GenericsInAction {
public static void main(String[] args) {
IntegerPrinter oldPrinter = new IntegerPrinter(188);
oldPrinter.print();
List<Integer> dates = oldPrinter.anyPrint(new ArrayList<Integer>(),7);
Integer x = dates.get(0);
}
}
我将代码浓缩到关键部分,并对其进行了轻微修改,以突出重要的行为:
class Ideone {
public static void main(String[] args) {
List<Integer> dates = new IntegerPrinter().anyPrint(7);
System.out.println(dates.get(0)); // succeeds
Integer x = dates.get(0); // Line 8, throws
}
}
class IntegerPrinter {
public <K> List<K> anyPrint(K lo) {
List<K> mylist = new ArrayList<>();
mylist.add((K) Float.valueOf(2.99f));
return mylist;
}
}
执行时,此程序将产生以下输出:
2.99
Exception in thread "main" java.lang.ClassCastException: class java.lang.Float cannot be cast to class java.lang.Integer (java.lang.Float and java.lang.Integer are in module java.base of loader 'bootstrap')
at Ideone.main(Main.java:8)
Ideone.com
演示
现在,让我们逐步了解代码,并尝试了解发生了什么
此行:
mylist.add((K) new Float(2.99f));
基本上告诉编译器;不关心类型,我们(作为程序员)保证它是K
,把它当作K
";。
然后,如果我们深入研究,我们会发现ArrayList
使用Object[]
作为后台数据结构。所以这里有一个问题,支持Object[] elementData
可以存储所有内容。
当我们开始检索元素时,事情会变得很奇怪。JLS对这些情况下的类型断言有些模糊(我认为它们包含在§5.1.5和§5.1.6.3中,但我不完全确定)。它基本上说";编译器必须断言类型,但仅在必要时才断言";。
因此,如果我们从List<Integer>
中检索一个元素,它显然不是Integer
,而是传递给一个可以处理Object
的方法,则不需要类型断言。这正是这里的情况:
System.out.println(dates.get(0));
System.out
中最接近的签名匹配是println(Object)
方法。这就是JLS§5.1.5中的情况:一个不断扩大的转换,它永远不会抛出。
另一方面,如果我们现在尝试检索Integer
并尝试将其存储在Integer
:中
Integer x = dates.get(0);
现在,类型检查已经到位。事实上,如果我们检查程序的输出,我们会看到System.out.println(...)
发生了,但对int
变量的赋值是触发ClassCastException
的语句。这就是JLS§5.1.6.3中描述的情况:运行时的缩小转换(来自ArrayList
的elementData(int)
方法)。
脚注
泛型无疑是JLS中最复杂、最令人困惑的部分之一。我尽量引用仲量联行的相关部分,但可能会被遗漏。我也知道这个问题以前被问过,但我找不到副本。如果:
- 对JLS的引用是错误的,应该引用另一部分,请通过评论或编辑帖子联系我
- 如果你发现(a)重复,请ping我,我会将问题作为重复关闭(如果可能的话,删除我的答案)
由于ArrayList
是一个类型擦除为java.lang.Object
的泛型类型,因此这就是存储在列表中的类型。您可以将类型擦除看作是运行时类型;真实的";类型,而不是编译器所知道的编译时类型。当程序运行时,任何类型都可以存储在ArrayList
中。
恰好,anyPrint
中K的类型擦除也是java.lang.Object
,因为你对类型K没有边界。该方法只编译一次,用于所有用途,它必须能够接受K的任何类型。因此,当编译anyPrint
的代码时,mylist.add( (K) new Float(2.99f));
行中对K的强制转换被忽略,因为K的类型删除是java.lang.Object
。铸造到java.lang.Object
是无用的和毫无意义的。它编译为mylist.add(new Float(2.99f));
,代码将类型为java.lang.Float
的对象插入类型为java.lang.Object
的列表中。
此外,Java中对对象类型的强制转换只需确保对象具有正确的类型,它不会像对基元类型的强制类型那样更改对象的值。因此,你没有理由相信2.99f的值会改变。
CCD_ 46单独编译。
在GenericsInAction
的main
方法中,K的参数化类型为java.lang.Integer
,因为您传入了一个通过自动装箱转换为java.lang.Integer
的7
,以与anyPrint
中java.lang.Object
的类型擦除兼容。因此,当编译main
方法时,编译器会在对dates.get
的调用之后插入一个运行时检查,一个检查串,该检查确保对dates.get(0);
日期的调用返回一个类型为java.lang.Integer
的对象,因为K的类型必须是main
中的java.lang.Integer
。
由于在列表中插入了java.lang.Float
,因此运行时检查失败并抛出ClassCastException
。