引言
通过前三篇文章,我们比较深入的理解了java中泛型的实现,并且知道了泛型可能带来的一些问题以及解决这些问题的方法。这篇文章,我会列举几个常见的泛型警告和错误,分析它们出现的原因,给出避免这些警告和错误的方案,相信有了前面对泛型擦除作为基础,本篇内容应该比较容易理解。
不允许的操作
因为编译期泛型被擦除,我们无法在运行期间知道确切类型,所以下面的三种操作都是不允许的:
public class Erased<T> {
public void f(Object arg){
/*if (arg instanceof T){
}*/
//T var = new T();
//T[] array = new T[10];
T[] array = (T[]) new Object[10];
}
}
不能对泛型使用instanceof,不能实例化泛型对象,不能实例化泛型数组。
解决方案
这里先只介绍instanceOf的解决方案。
泛型不能使用instanceof,这个可以通过使用动态的isInstance()来解决,看下面的例子:
public class Erased<T> {
Class<T> kind;
public Erased(Class<T> kind) {
this.kind = kind;
}
public boolean f (Object arg){
return kind.isInstance(arg);
}
public static void main(String[] args) {
Erased<Fruit> fruitErased = new Erased<>(Fruit.class);
System.out.println(fruitErased.f(new Fruit()));
System.out.println(fruitErased.f(new Apple()));
Erased<Apple> appleErased = new Erased<>(Apple.class);
System.out.println(appleErased.f(new Fruit()));
System.out.println(appleErased.f(new Apple()));
}
}
我们在泛型类中添加了一个用来表示泛型类型的Class对象,然后通过该Class对象的inInstance方法来达到instanceof的目的。
当泛型遇上重载
看下面的例子:
public class FruitContainer {
public void process(List<Fruit> fruits){
System.out.println(fruits.size());
}
public void process(List<Apple> apples){
System.out.println(apples.size());
}
}
这两个重载的方法是不能编译成功的,提示错误信息为:
process(List<Fruit>)' clashes with 'process(List<Apple>)'; both methods have same erasure
这个应该比较好理解,泛型类型擦除之后,这两个方法的参数实际上都是List ,擦除动作导致这两个方法的特征签名变得一模一样。
为了解决这个问题,我们可以直接去掉重载:
public class FruitContainer {
public void processFruits(List<Fruit> fruits){
System.out.println(fruits.size());
}
public void processApples(List<Apple> apples){
System.out.println(apples.size());
}
}
这样, 通过不同的方法名称来区分方法,很轻松的解决了这个问题。
记住这个编译错误,当你下次碰到时,就能很快解决它。
可能出现堆污染风险的警告
还是看之前在桥接方法中看过的示例,我把main方法修改了一下:
public class ObjectContainer<T> {
private T contained;
public ObjectContainer(T contained) {
this.contained = contained;
}
public T getContained() {
return contained;
}
public void setContained(T contained) {
this.contained = contained;
}
}
public class FruitContainer extends ObjectContainer<Fruit> {
public FruitContainer(Fruit contained) {
super(contained);
}
@Override
public void setContained(Fruit contained) {
super.setContained(contained);
}
public static void main(String[] args) {
FruitContainer fruitContainer = new FruitContainer(new Apple());
ObjectContainer objectContainer = fruitContainer;
objectContainer.setContained("this is a error parameter");
}
}
我们让原始类型ObjectContainer指向了创建的FruitContainer对象,然后调用了objectContainer的setContained方法,先看结果:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to person.andy.concurrency.generic.fruits.Fruit
at person.andy.concurrency.generic.erased.FruitContainer.setContained(FruitContainer.java:7)
at person.andy.concurrency.generic.erased.FruitContainer.main(FruitContainer.java:22)
调用setContained方法抛出了java.lang.ClassCastException异常,这是为什么呢?因为 objectContainer.setContained(“this is a error parameter”);调用的实际上是编译器为FruitContainer自动生成的桥接方法,因为该方法的参数是Object,所以编译没有问题,但是该方法里面为了调用setContained(Fruit fruit)方法而进行了强制类型转换,错误就在这里出现了,这就是堆污染。
关于堆污染,可以参考这里。
出现这种问题的原因是我们使用了泛型的原始类型即ObjectContainer,我们的代码中不应该使用泛型的原始类型,我们应该这样来使用:
public static void main(String[] args) {
FruitContainer fruitContainer = new FruitContainer(new Apple());
ObjectContainer<Fruit> objectContainer = fruitContainer;
//下面这行代码不能编译通过
objectContainer.setContained("this is a error parameter");
}
这样我们就不能对setContained方法传入其他类型了。
在实际中,我们总会碰到这样的警告:
List<String> list = new ArrayList<>();
List list1 = list;
list1.add(12);
System.out.println(list.get(0));
这个示例中的最后一行也会出现ClassCastException异常。所以我们在代码中应该禁止使用泛型的原始类型。IDE遇到这样代码也会给出提示:
提示信息一般是:Raw use of parameterized class xxx;当我们代码中出现这种警告时,就应该小心了。
小结
这篇文章简单总结了在使用泛型过程中可能出现的错误和异常,java泛型的特殊实现使得很多问题需要额外增加一些机制来解决或者避免,我们在编码过程中可能经常遇到这些警告,理解他们的发生原因,我们就能解决这些错误和异常。