Java泛型
JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进。泛型允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。
Java泛型由来的动机
理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting),例如:
List<Apple> box = ...;
Apple apple = box.get(0);
box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:
List box = ...;
Apple apple = (Apple) box.get(0);
泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。
泛型的构成
由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,因此产生于以下几种情况:
- 泛型类声明
- 泛型接口声明
- 泛型方法声明
- 泛型构造器(constructor)声明
泛型类和接口
如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面。
Demo:
public interface List<T> extends Collection<T> {
}
类型变量就如同一个参数,它提供给编译器用来类型检查的信息。
### 泛型方法和构造器(Constructor)
如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。
Demo:
public static <T> T getFirst(List<T> list){
}
这个方法将会接受一个List
泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
关于子类型
在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:
在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。所有Java类型都是Object类型的子类型。
B类型的任何一个子类型A都可以被赋给一个类型B的声明:
Apple a = ...;
Fruit f = a;
泛型类型的子类型
一个Apple对象的实例可以被赋给一个Fruit对象的声明,但是,一个List<Apple>却不能赋值给一个List<Fruit>。
这意味着下面的这段代码是无效的:
List<Apple> apples = ...;
List<Fruit> fruits = apples;
泛型类型跟其是否子类型没有任何关系。
类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List
Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。
类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Intege\r>创建的对象,都是共享一个静态变量。
泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。
类型通配符
类型通配符一般是使用 ? 代替具体的类型实参。
因为**泛型类型跟其是否子类型没有任何关系。**所以,类似于Box
public class GenericTest {
public static void main(String[] args) {
Box<Number> name = new Box<Number>(99);
Box<Integer> age = new Box<Integer>(712);
getData(name);
//The method getData(Box<Number>) in the type GenericTest is
//not applicable for the arguments (Box<Integer>)
getData(age); // 1
}
public static void getData(Box<Number> data){
System.out.println("data :" + data.getData());
}
}
class Box<T> {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
下面的代码是无法编码通过的。
怎么解决上面的问题呢?
这时候可以通过通配符来解决:
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
##类型通配符上限与下限
类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反。
上限demo:
public static void getData(Box<? extends Number> data) {
System.out.println("data :" + data.getData());
}
Box<? extends Number>:参数类型Box的泛型必须是Number的子类
下限demo
public static void getData(Box<? super Number> data) {
System.out.println("data :" + data.getData());
}
Box<? super Number>:参数类型Box的泛型必须是Number的父类
泛型方法
泛型方法定义如下:
修饰符 <T> T 方法名(Class<T> c){
方法体
}
定义泛型方法时,必须在返回值前边加一个
编写Java泛型方法,返回值类型和至少一个参数类型应该是泛型,而且类型应该是一致的。
下面主要介绍两种十分相似的java泛型方法的使用以及它们之间的区别。
public static <T extends CommonService> T getService(Class<T> clazz) {
T service = (T) serviceMap.get(clazz.getName());
if (service == null) {
service = (T) ServiceLocator.getService(clazz.getName());
serviceMap.put(clazz.getName(), service);
}
return service;
}
public static <T> T getService(Class<? extends CommonService> clazz) {
T service = (T) serviceMap.get(clazz.getName());
if (service == null) {
service = (T) ServiceLocator.getService(clazz.getName());
serviceMap.put(clazz.getName(), service);
}
return service;
}
这两个泛型方法只有方法的签名不一样,方法体完全相同。
第一种泛型方法:返回值和参数值的类型是一致,推荐使用;
第二种泛型方法:返回值和参数值的类型不是一致,请谨慎或避免使用。