关于Java泛型的一个非常隐蔽的问题
数组在两个方面与泛型类型存在着不同。首先,数组是协变的。这个词语看起来很高深,但实际上却很简单,它表示如果Sub是Super的子类型,那么数组类型Sub[]就是Super[]的子类型。与之相反,泛型是逆变的:对于任意两个不同的类型Type1与Type2来说,List
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
但如下代码片段却是非法的:
// Won't compile!
List
ol.add("I don't fit in");
无论哪种方式,你都不能将String放到Long容器中,不过对于数组来说,你是在运行期发现的错误;而对于列表来说,你是在编译期发现的错误。当然了,你更愿意在编译期发现错误。
数组与泛型之间的第二个主要差别在于数组是具化的[JLS, 4.7]。这意味着数组在运行期是知道其元素类型的,并且会强制施加类型限制。如前所述,如果试图将String放到Long数组中,那就会抛出ArrayStoreException异常。与之相反,泛型是通过类型擦除实现的[JLS, 4.6]。这意味着泛型只会在编译期施加类型限制,在运行期则会丢弃(或是叫做擦除)其元素类型。类型擦除使得泛型类型能够与没有使用泛型的遗留代码互操作(条款26),这确保了Java 5中向泛型的平滑迁移。
由于这些根本差别,数组与泛型是不能混用的。比如说,创建泛型类型数组、参数化类型数组以及类型参数数组都是不合法的。因此,如下数组创建表达式都是不合法的:new List
为什么说创建泛型数组是不合法的呢?因为它不是类型安全的。如果合法,那么由编译器在正确的程序中所生成的类型转换就有可能在运行期出现失败,抛出ClassCastException异常。这违背了泛型类型系统所提供的基本保证。
具体来说说,考虑如下代码片段:
// Why generic array creation is illegal - won't compile!
List
List
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
假设第1行代码(创建了一个泛型数组)是合法的。第2行代码创建并初始化了一个包含单个元素的List
从技术角度来说,诸如E、List
禁止创建泛型数组挺恼人的。比如说,这意味着我们通常无法让泛型集合返回一个其元素类型的数组(不过,请参考条款33了解如何部分地解决这个问题)。此外,在同时使用可变参数方法(条款53)与泛型类型时,你会遇到令人困惑的警告信息。这是因为,每次调用可变参数方法时,系统都会创建一个数组来持有可变参数。如果该数组的元素类型并非具化的,那就会遇到警告。SafeVarargs注解可用来解决这个问题(条款32)。
当遇到泛型数组创建错误或是在转换为数组类型时出现未检查的类型转换警告时,最佳的解决方案通常是使用集合类型List
比如说,假设你要编写一个Chooser类,其构造方法接收一个集合,它还提供了一个方法,随机返回集合中的一个元素。根据向构造方法所传递的集合的不同,你可以将该选择器作为一个骰子、一个魔术八球,或是一个蒙特卡罗模拟的数据源。如下是不使用泛型的一个简化的实现:
// Chooser - a class badly in need of generics!
public class Chooser {
private final Object[] choiceArray;
public Chooser(Collection choices) {
choiceArray = choices.toArray();
}
public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
要想使用这个类,你需要在每次调用choose方法时将其返回值从Object转换为所需的类型,如果转换错误,那么在运行期就会失败。根据条款29,我们将Chooser改为泛型的。变化的部分如粗体所示:
// A first cut at making Chooser generic - won't compile
public class Chooser
private final T[] choiceArray;
public Chooser(Collection
choiceArray = choices.toArray();
}
// choose method unchanged
}
如果编译这个类,你会看到如下错误消息:
Chooser.java:9: error: incompatible types: Object[] cannot be
converted to T[]
choiceArray = choices.toArray();
^
where T is a type-variable:
T extends Object declared in class Chooser
你可能觉得没什么大不了的,将Object数组转换为T数组就行了:
choiceArray = (T[]) choices.toArray();
这么做会消除掉错误,但却会收到一个警告:
Chooser.java:9: warning: [unchecked] unchecked cast
choiceArray = (T[]) choices.toArray();
^
required: T[], found: Object[]
where T is a type-variable:
T extends Object declared in class Chooser
编译器告诉你,它无法保证运行期类型转换的安全性,因为程序不知道T表示的类型是什么——记住,元素类型信息会在运行期擦除掉泛型。那么程序会正常运行么?是的,不过编译器却不敢保证。你可以自己保证这一点,将证据放在注释中,并使用注解来压制警告,不过最好还是从根源上消除掉警告(条款27)。
为了消除掉未检查的类型转换警告,请使用列表来代替数组。如下这个版本的Chooser类会正常通过编译,并且没有错误和警告信息:
// List-based Chooser - typesafe
public class Chooser
private final List
public Chooser(Collection
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
该版本有点啰嗦,运行速度也可能稍微有点慢,不过却是值得的,因为运行期不会再抛出ClassCastException了。
总结一下,数组与泛型拥有非常不同的类型规则。数组是协变且具化的;泛型是逆变且类型擦除的。因此,数组提供了运行期的类型安全,而非编译期的类型安全;泛型则是完全相反的。作为一条原则,数组与泛型不应该混用。如果混合使用了数组与泛型,并且收到了编译期的错误或是警告,那么首先就应该考虑使用列表来代替数组。