1. 问题来源
今天在刷题时,遇到了需要使用泛型数组的场景。题目是按之字形打印二叉树。这道题目需要交替使用两个栈来解决,我的初始代码为:
ArrayDeque<TreeNode>[] stacks = new ArrayDeque<TreeNode>[2]; //1
stacks[0] = new ArrayDeque<TreeNode>();
stacks[1] = new ArrayDeque<TreeNode>();
而在编译时,代码1
报出了如下编译错误:
Cannot create a generic array of ArrayDeque<Integer>.
理解和解决这个问题的过程中我对Java泛型的理解又深入了一点,所以才有这篇文章。
2. Java“禁止”泛型数组
查阅资料,找到了The Java™ Tutorials: Generics,其中讲到了泛型数组,并说道:除非使用通配符,否则一个数组对象的元素不能是泛型。这么做的原因,是为了防止下述代码产生的类型安全
问题:
// Not really allowed.
List<String>[] lsa = new List<String>[10]; //1
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0); //2
如果允许泛型数组的存在(第1处代码编译通过),那么在第2处代码就会报出ClassCastException,因为lsa[1]是List<Integer>。Java设计者本着首要保证类型安全(type-safety)的原则,不允许泛型数组的存在,使得编译期就可以检查到这类错误。
3. 解决方案
但是连Java的设计者也承认,这样在使用上很令人恼火(原文是annoying),所以提供了变向的解决方案:显式类型转换。
3.1 通配符
The Java™ Tutorials: Generics给出的解决方案如下:
List<?>[] lsa = new List<?>[10]; //1
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0); //2
在第1处,用?
取代了确定的参数类型。根据通配符的定义以及Java类型擦除的保留上界原则,在2处lsa[1].get(0)
取出的将会是Object
,所以需要程序员做一次显式的类型转换。
3.2 反射
使用java.util.reflect.Array
,可以不使用通配符,而达到泛型数组的效果:
List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4); //1
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = lsa[1].get(0); //2
可以看到,我们利用了Array.newInstance()
生成了泛型数组,这里没有使用任何通配符,在第2处也没有做显式的类型转换,但是在第1处,仍然存在显式类型转换。我在牛客网刷题时遇到的问题,就是通过这种方式来解决的。
3.3 总结
要想使用泛型数组
,要求程序员必须执行一次显示的类型转换,也就是将类型检查的问题从编译器交给了程序员。实际上Java的设计者正是此意,在(List<String>[])Array.newInstance(ArrayList.class, 4)
处会有一个unchecked warning
,正是编译器在提醒程序员:这个地方,我不会帮你做类型检查,你要自己小心!
。
4. 有趣的问题
在探究泛型数组的过程中,我写了一些实验代码,其中有一段:
List<Integer>[] lsa = (List<Integer>[])Array.newInstance(ArrayList.class, 4);
Object o = lsa;
Object[] oa = (Object[]) o;
List<String> li = new ArrayList<String>();
li.add("asdf");
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
System.out.println(lsa[1].get(0)); //2
这段代码不会在2处抛出ClassCastException,相反,它运行正常,输出的结果是asdf。这打破了我的理解,为什么从一个List<String>中取出一个Integer不会报错呢?
如果将上述代码中参数类型的String与Integer互换:
List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4);
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
System.out.println(lsa[1].get(0)); //2
2处的ClassCastException就会正常抛出。
为了找到原因,我查看了这两段代码的字节码。下图是第一段代码的字节码:
这是第二段代码的字节码:
问题的原因找到了:第一段代码中,lsa[1].get(0)取出的Integer没有做类型检查(checkcast),而在第二段代码中做了类型检查。
为什么会这样呢?我分析的原因是:第一段代码中,编译器发现下一行调用PrintStream.println,参数类型还是Object(因为System.out.println()这个方法,对于引用类型只有char[]、String、Object三类参数,没有Integer),所以编译器在此处做了优化,免去了类型检查和类型转换的处理。
5. 总结
- Java为了保证
类型安全
,牺牲掉了泛型数组的使用灵活性,程序员想使用的话,必须进行显式的类型转换。 - 第4节介绍的问题,表明编译器并不是在泛型被使用的每一处都进行了类型检查与类型转换,还是存在一定优化的。所以我在Java泛型的实现:原理与问题这篇博文中,说“Java的泛型是一种违泛型,编译为字节码时参数类型会在代码中被擦除,单独记录在Class文件的attributes域内,而在使用泛型处做类型检查与类型转换”,最后一句话是不严谨的。