关于 Java 的泛型

根据《Java 核心技术卷 1》的第 8 章稍微了解一下泛型。

Java 的泛型归根结底是一种语法糖,当我们定义一个泛型类,比如 Pair<T,U>,又去声明两个实现,比如 Pair<String, Integer>,Pair<Integer, LocalDate>,这并不会真的在编译期多整出两个类出来(C++的模版是这样干的),在运行时,泛型类其实归根结底只有一个raw type

考虑如下代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Pair<T,U> {
public T first;
public U second;
Pair(T a, U b) {
first = a;
second = b;
}
}
class Pair { // 类型擦除后的 raw type,编译器会自动处理类型转换
public Object first;
public Object second;
Pair(Object a, Object b) {
first = a;
second = b;
}
}
Pair<Integer,String> a = new Pair<>(123,"456");
System.out.println(a.getClass()); // Class Pair
System.out.println(a.second.length());

可以看到,a.getClass()并不能返回什么 Class Pair<Integer,String>,编译期会将泛型的类型擦除(擦除后获得的 raw type 为它继承的第一个类/接口,如果没有,则是 Object,所以本书建议将带方法的接口放在前,而标签接口,如RandomAccess等接口放到最后,以防止添加更多类型转换),所有的泛型类的实例其实都是raw Type类型。只不过编译器会自动处理类型转换罢了。

为什么要泛型

泛型机制相较于杂乱地使用 Object 变量进行强制类型转换的代码来说更有安全性和可读性,它对使用容器类型尤其有用。

我认为 Java 中的泛型,实际上就是编译期的类型检查机制,对变量类型进行各种各样的约束,从而保证不出错误,同时也方便编写。比如,要求泛型类必须实现 Comparable 接口(这个接口也是泛型的,它标识能够与什么对象进行比较),这样就能够方便大胆地对泛型变量调用compareTo方法而不用担心抛出异常,因为这是编译器保证了的。

比如,我之前按照《算法》中的排序算法,编写如下代码——

1
2
3
4
5
6
7
public static void bubbleSort(Comparable[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) // 每一次外层排序排好一个数
for (int j = len - 1; j > i; j--)
if (less(arr[j], arr[j - 1]))
exch(arr, j, j - 1);
}

这里,将 Comparable 数组作为参数,它抛出一个警告,要求不要使用 raw type,这里可以改成Comparable<T>,但也可以这样改——

1
public static <T extends Comparable<T>> void sort(T[] arr) { //...

好家伙,这样不是看起来更舒服一些?而且能让编译器替我们做检查,妙哇!

extends 和 super

泛型中可以有两个关键字——extendssuper

泛型之前

Java 在提供泛型之前,泛型是通过继承实现的(啥?)。考虑没有泛型的 ArrayList——

1
2
3
4
5
public class ArrayList { // before generic classes
private Object[] elementData; // 其实现在也是这样的
public Object get(int i) { . . . }
public void add(Object o) { . . . }
}

当我们试图获取一个值的时候,必须要对它进行强制类型转换——

1
2
3
4
5
ArrayList files = new ArrayList();
files.add("123"); // 这是完全没有约束的,全靠程序员“自觉”
String filename = (String) files.get(0); // 有点麻烦,还可能出事,因为……
files.add(123); // 合法的!
String anotherOne = (String) files.get(0); // 要抛 ClassCastException 异常啦

显然,这里如果加入泛型,就能在编译期进行语法检查,防止这种操作了。

为什么不能声明泛型数组?

原因就是,会绕过类型检查。编译器无法发现类型错误,

考虑如下代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Pair<T1,T2>{
T1 first;
T2 second;
Pair(T1 t1,T2 t2) {
this.first=t1;
this.second=t2;
}
}

Pair<Integer,String>[] arr = new Pair<>[10]; //这里只能用 raw type 才能过编译
arr[0] = new Pair<Integer,String>(123,"456"); // 正常操作
arr[1] = new Pair<String,Double>("www",1.0); // 过不了
Object[] objArr = (Object[]) arr;
objArr[1] = new Pair<String,Double>("www",1.0); // No Problem!

假设泛型数组是合法的,则用户可以向上转型,将其转化为Object数组,然后向其插入其它类型的该泛型,这在语法上是没有任何问题的——它们的 raw type 都是一样的嘛,所以不会抛java.lang.ArrayStoreException(如果试图向该Object数组里插入别的类型就会抛这个异常),只有到使用中才会发现问题,抛出java.lang.ClassCastException

总之,泛型数组会导致编译期的泛型机制失去约束的意义,需要泛型数组应当使用ArrayList

但是,泛型数组可以作为方法参数传入,还蛮奇妙的。或许是因为,作为函数参数使用时,其使用是方法设计者规定,而非是用户规定的,所以它是被“封装”了的,能够避过用户的骚操作。这是 Java 对方法设计者的信任吗?