Java泛型

概要

泛型在Java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

  • 泛型的定义

    泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参,而顾名思义,参数化类型就是将类型原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

  • 为什么使用泛型

    泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数可以作用于类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

类型擦除

Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。如在代码中出现的List和List等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能地发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

很多泛型的奇怪特性都与这个类型才擦除的存在有关,包括:

  • 泛型类并没有自己独有的Class类对象。比如不存在List.class,而只有List.class
  • 静态变量是被泛型类的所有实例所共享的。对于声明为AClass的类,访问其中的静态变量定位方法仍然是AClass.staticFun()。不管是通过AClass还是AClass创建的对象,都是共享一个静态变量
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyExpection和MyExpection的。对于JVM来说,它们都是MyExpection类型的,也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object,如果指定了类型参数的上界,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成Object get(),List就变成了List。接下来就可能生成一些桥接方法。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码。

1
2
3
4
5
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}

当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object) 方法。这个时候就由编译器来动态生成这个方法。

通配符

在使用泛型类的时候,既可以指定一个具体的类型,如List就声明了具体的类型是String,也可以用通配符?来表示未知类型。正因为类型未知,就不能通过new ArrayList()方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于List中的元素却总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。如List<? extends SuperClass> 说明List中可能包含的元素类型是SuperClass及其子类,而List<? super ChildClass>则说明List中包含的是ChildClass及其父类。当引入上下界之后,在使用类型的时候就可以使用上下界 中定义的方法。