泛型程序设计
1. 为什么要使用泛型程序设计?
泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用。能够提高代码的复用性,比如ArrayList
就可以收集任何类的对象,比如String
,Integer
等,而不用为这些对象去编写不同的类。
1.1 类型参数
泛型允许在定义类和接口的时候使用类型参数(type parameter),声明的类型参数在使用时使用具体的类来替换,比如下面这个例子
List<String> files = new ArrayList<>();
可以很明显的看出数组中包含的是String
对象,类型参数使得程序更易读,也更安全。
2. 定义简单泛型类
泛型类(generic class)就是有一个或多个类型变量的类。下面定义了一个简单的泛型类Pair
Pair
类引入了一个类型变量T
,用于指定方法的返回类型,字段和局部变量的类型
public class Pair<T>
{
private T first;
private T second;
public Pair() { first = null; second = null; }
public Pair(T first, T second) { this.first = first; this.second = second; }
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
3. 泛型方法与类型变量的限定
我们再定义了泛型类之后,就可以设计带有类型参数的方法,比如我们想要寻找数组中元素的最大最小值,就可以这样定义
public static <T> T min(T[] a) {
// blablabla
}
但是这样的设计有一个问题,在方法内部对元素进行比较时,需要使用compareTo
方法,这就要求类型T
实现Comparable
接口,所以需要对其进行限定(bound)
public static <T extends Comparable> T min(T[] a) {
// blablabla
}
之所以使用extends
而不是使用implements
是因为T
是限定类型的子类型,而继承关系更接近于这种概念,一个类型变量也可以有多个限定
T extends Comparable & Serializable
下面使用我们定义的泛型类实现了一个寻找数组元素最大和最小值的方法。
import java.time.*;
public class PairTest2
{
public static void main(String[] args)
{
LocalDate[] birthdays =
{
LocalDate.of(1906, 12, 9), // G. Hopper
LocalDate.of(1815, 12, 10), // A. Lovelace
LocalDate.of(1903, 12, 3), // J. von Neumann
LocalDate.of(1910, 6, 22), // K. Zuse
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg
{
/**
Gets the minimum and maximum of an array of objects of type T.
@param a an array of objects of type T
@return a pair with the min and max values, or null if a is null or empty
*/
public static <T extends Comparable> Pair<T> minmax(T[] a)
{
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 1; i < a.length; i++)
{
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
4. 泛型代码和虚拟机
虚拟机没有泛型类型对象,所有对象都属于普通类。
4.1 类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名。同时,类型变量会被擦除(erased),并替换为其限定类型。
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换 2.移除所有的类型参数。
比如3中的例子,原始类型就会变成下面的样子
public class Pair {
private Object first;
private Object second;
}
由于T
是一个无限定的变量,所以直接用Object
替换。
4.2 转换泛型表达式
调用一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst
方法擦除类型后的返回类型是Object
,编译器自动插入转换到Employee
的强制类型转换。
4.3 转换泛型方法
类型擦除还会出现在泛型方法中,比如
public static <T extends Comparable> T min(T[] a)
在擦除类型之后,就会变成
public static Comparable min(T[] a)
5. 限制与局限性
5.1 泛型遇到重载
public class A {
public static void method(List<Integer> list) {
}
public static void method(List<String> list)
}
上述代码编译器会报错,提示两个方法冲突,因为它们被擦除之后都变为List
5.2 泛型类中静态变量无效
public class Singleton<T> {
private static T singleInstance;
public static
}
静态变量被泛型类的所有实例对象所共享。
5.3 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象,泛型类扩展Throwable
不合法。
6. 泛型类的继承规则
考虑一个父类和子类,比如Employee
和Manager
,需要注意的是,Pair<Manager>
不是Pair<Employee>
的子类。
也就是说,无论S
和T
又怎样的关系,Pair<T>
和Pair<S>
没有任何关系,这对于类型安全非常重要。
参考:
- 《Java核心技术 卷I》
- Java成神之路