My Avatar

胡湘铭的博客

Coding and thinking!

学习笔记之Java泛型

2016年4月21日, 发表于 长春

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

有限制的泛型–关于擦除

Java SE5开始支持泛型,由于兼容性的原因,采用了擦除的方法,这导致了很多问题。本文是我的学习笔记,不对之处希望指出!

1.泛型的Class类型

1
2
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();

此时的c1==c2是成立的,它们都为ArrayList.class即Class< ArrayList >的对象(单例),因为类型被擦除了。

2.泛型的参数标识符

字节码是没有泛型这一概念的,java文件编译成字节码时,泛型参数类型被擦除,替换为它们的非泛型上界(举个例子,若默认边界,即占位符T被替换为Object)。

此外,在泛型代码内部,无法获得任何有关泛型参数的信息。例: getTypeParameters()这个函数将返回一个TypeVariable数组,表示有泛型声明所声明的类型参数,但实际上,它返回的仅仅是用作参数占位符的标识符,例如定义一个类时

1
class MyClass<T>{}

getTypeParameters()将返回[T]。

3.泛型数组与泛型的实例化

(1)不能直接创建带泛型参数化的数组。

原因是泛型数组会破坏类型安全。 举个例子,Object[]数组是任何数组的父类,任何一个数组都可以向上转型成Object[],假设原始数组为String[],我把它转型为Object[],现在往里添加一个Integer元素,编译不会有问题,因为Integer也说Object的子类,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException。:

1
2
3
String[] strArray = new String[20];
Object[] objArray = strArray;
objArray[0] = new Integer(1); //ArrayStoreException

现在我们开始考虑泛型

1
2
3
4
5
Map<Integer, String>[] mapArray = new HashMap<Integer, String>[20];//ERROR
Map<Object,Object>[] mapArray2 = mapArray;
Map<Integer, Double> errorMap = new HashMap<Integer, Double>();
error.put(1,1.0);
mapArray2[0] = errorMap;//检测不出错误

这样不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,因为泛型的类型擦除,我们定义的String已经被擦除了,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,结果会是灾难性的,所以索性不允许创建带有泛型参数化的数组。 数组必须牢记它的元素类型,也就是所有的元素对象都必须一个样,泛型类型数组做不到。

(2)任何在运行时需要知道确切类型信息的操作都无法工作。

1
2
3
4
5
6
7
8
9
10
//代码摘自Thinging in Java
public class Erased<T>{
    private final int SIZE = 100;
    public static void f(Object arg){
        if(arg instance of T){}//Error 1
        T var = new T()//Error 2
        T[] array = new T[SIZE]//Error 3
        T[] array = (T)new Object[Size]//Error 4
    }
}

个人拙见,原因可能是:1.由于擦除,编译器不知道T的具体类型,如果要new或其他需要知道确切类型信息的操作,只能将T擦除到边界处,在这即Object,而我们在使用泛型类的时候,编译器自动将Object转型为我们需要的类型,但是,由于我们new(或其他操作)的是Object,就会发生类型转换错误,这是不被允许的。2.不能实例化的另一原因是因为编译器不能验证T具有默认无参构造器。 解决方案: ERROR 1:可以采用动态的isInstance(),定义一个Class< T > kind,然后用kind.isInstance(arg)判断。 ERROR 2:思路是传递一个工厂对象,来创建新实例,例如Class对象的newInstance(),如果想要的对象没有默认无参构造器,需要用反射的方法获得构造器进行实例化。 ERROR 3和4:最简单的方法是使用ArrayList,如果仍旧希望创建数组,可以使用java.lang.reflect中的Array.newInstance(),例子:

1
2
3
4
5
6
7
8
9
10
import java.lang.reflect.*;
public class GenericArray<T>{
    private T[] array;
    @suppressWarnings("unchecked")
    public GenericArray(Class<T> type,int size){
        array = (T[])Array.newInstance(type,size);
    }
    /***
    ***/
}

4.泛型与重载

我们已经知道泛型会有擦除,所以List和List在JVM的眼中是一样的,重载时就会出现问题:

1
2
3
4
5
6
7
8
9
10
public class GenericTypes {  

    public static void method(List<String> list) {  
        System.out.println("List<String> list");  
    }  

    public static void method(List<Integer> list) {  
        System.out.println("List<Integer> list");  
    }  
}  

JVM不能识别出这两个方法的区别。

5.静态变量

由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,不管实例化的参数是哪种类型,所有静态变量是共享的。

6.关于通配符

(1)泛型与通配符的继承关系

1
2
    class Father{}
    class Son extends Father{}

在这里Father是Son的超类,但是ArrayList并不是ArrayList的超类,实际上,它们并没有多大的关系。 然而,对于通配符ArrayList,ArrayList和ArrayList都是ArrayList的子类,对于通配符ArrayList,ArrayList和ArrayList都是ArrayList的子类,此外,ArrayList是它们的共同超类。

(2)通配符的协变与逆变

1
2
3
4
5
6
7
8
9
class Son2 extends Father{}
List<? extends Father> fatherList = new ArrayList<Son>();
//Compile Error
//fatherList.add(new Son());
//fatherList.add(new Son2());
fatherList.add(null);//OK
Father f = fatherList.get(0);//OK
fatherList.contains(new Son());//OK
fatherList.indexOf(new Son());//OK

查看ArrayList的文档,可知原因在于add()的参数是一个具有泛型参数类型的参数,在这里就是? extends Father,而编译器不知道到底需要Father的哪个子类型,所以它不会接受任何类型的参数(null除外)。而contains()和indexOf()的参数为Object,因此允许此调用。我们平时设计泛型类时也可以参考这种方法。 而与之相反的超类型通配符<? super Son>则不同:

1
2
3
4
5
class GrandSon extends Son{}
List<? super Son> sonList = new ArrayList<Son>();
sonList.add(new Son());
sonList.add(new GrandSon());
//sonList.add(new Father()); //ERROR

由于sonList是Son的某种基类的list,根据向上转型的规则,往里添加任何SOn或者Son的子类都是可以的,但是,往sonList里添加Father是不安全的,因为编译器不知道sonList到底是Son的哪种基类的List。

7.一个类不能实现同一个泛型接口的两种变体

1
2
3
4
5
6
7
interface Payable{void method(){}}
class Employee implements Payable{}
class Hourly extends Employee implements Payable{}

interface Payable2<T>{}
class Employee2 implements Payable2<Employee2>{}
class Hourly2 extends Employee2 implements Payable2<Hourly2>{}//ERROR

这是Thingking in java的内容,但是并没有讲详细,为什么前者可通过编译,而后者不可以? 第一个普通接口编译通过,因为即使继承的类实现了同样的接口,也可以被覆盖。 (我自己的理解,若有错误之处,希望指出!)而对于泛型接口,由于擦除的原因,泛型参数类型被擦除到边界,在这里也就是Object,这时实现了接口的类需要一个桥方法实现覆盖(桥方法的详细内容可以谷歌),Java的编译器自动帮我们完成了这项任务,生成了一个参数类型是Object的桥方法,这个方法自动调用我们实现的方法,现在问题来了,在Hourly2中,桥方法调用的究竟是接口Payable还是Payable中的方法呢?编译器不知道,所以错误。 简单的说,如果Payable2接口中有一个方法 public void setData(T data){/*...*/},编译成字节码后变成public void setData(Object data){/*...*/} ,所以Employee2和Hourly2中必须有覆盖了这个方法的实现,即参数为Object的setData桥方法,在Employee2中,它自动调用public void setData(Employee2 data){/*...*/},然而在Hourly2中,编译器就不知道该调用哪个了。