Java-6-泛型编程

在一些情况下,我们的 类 或者 方法 需要能够处理多种 数据类型,例如 数组列表 ArrayList<T>,可以存储任意类型的元素 T。对于 java, 由于泛型概念为后期添加的内容,为了实现与 早期 java 版本的兼容,java 的泛型编程有很多特殊的限制。

A. 声明与调用

通过例子来看 泛型类 的声明和调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类的内容
public class Entry<K key, V value>{
private K key;
private V value;

public Entry(K key, V value){
this.key = key;
this.value = value;
}
public K getKey(){return key;}
public V getValue(){rturn value;}
}

//实例化
Entry<String, Integer> entry = new Entry<>("Fred", 42);
// 后面的构造函数 等同于 new Entry<String, Integer>("Fred", 42);

从上面的例子中,我们可以看到,我们 写 泛型类的时候,提供 K, V 作为类型参数, 让它们 以类似形参的 形式 传入 变量的类型,最终在实例化的 时候,再具体指明 所处理的参数变量 类型。

实例化时的构造函数可以 省略 尖括号中的内容, Java 将会根据前面的 声明类型自动推断。 (<> bracket pair 也叫做 diamond

类似泛型类,泛型方法 同样在方法声明时引入类型参数。泛型方法可以出现在泛型类或者普通类中。 同样我们通过一个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在普通类 中 的泛型方法 
public class Arrays{
public static <T> void swap(T[] array, int i, int j){
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}

// 泛型方法的 调用
String[] friends = ...;
Arrays.swap(friends, 0, 1);
// java 自动推断 泛型方法的 类型参数。
// 也可以写作: Arrays.<String>swap(friends, 0, 1);

需要注意的是,泛型类和泛型方法 中的类型参数 不允许基本数据类型(可以使用基本数据类型的包装类)。

B. 类型的边界与通配符

a. 构建泛型类/方法时

在构建泛型类或泛型方法的时候,我们可以通过规定泛型类/函数中的类型参数继承某个类或者实现某个接口,来对传入的类型进行 限制(type bounds)。看一个例子:

1
2
3
4
public static <T extends AutoCloseable> void closeAll(ArrayList<T> elems) 
throws Exception{
for (T elem : elems) elem.close();
}

上面的 extends 表示 T 类型 需要是 AutoCloseable 的 子类型(subType)(这里只是借用了 继承 的关键字,但并不表示 类继承的意思,无论边界为接口还是类,都使用 extends), 因此,我们在程序中可以调用 close() 方法。

我们可以为 类型参数 设置多个边界,e.g. T extends Runnable & AutoCloseable, 需要注意的是,边界中可以有多个接口,但只能有一个类,并且类需要放在第一个。

b. 通配符

对于 Java,数组类型支持将子类的元素赋值给超类的引用,i.e. Employee staff = new Manager[10]; 由于 Manager 为 Employee 的子类。这种行为称之为 covariance (协变, ref) 。而使用 泛型作为传入参数的类型和方法则不支持这一机制,例如:不允许 将 ArrayList<Manager> 的元素 赋值给 ArrayList<Employee> 类型的变量。

对于泛型类,我们通过通配符来实现类型的 协变 or 逆变(covariance,contravariance)。我们通过下面两个例子来介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 子类通配符的例子
public static void printNames(ArrayList<? extends Employee> staff){
for(int i = 0; i < staff.size(); i++){
Employee e = staff.get(i);
System.out.println(e.getName());
}
}

// 超类通配符例子
public interface Predicate<T>{
// predicate 接口,用来测试一个类型的对象有没有 特定的属性
boolean test(T arg);
}
// 调用下面的函数时,需要需要保证传入的 Predicate 对象必须实现了 filter() 方法。
// 调用时,实现 filter() 方法的过程可以通过 lambda 表达式来实现。
public static void printAll(Employee[] staff, Predicate<? super Employee> filter){
for(Employee e : staff){
if(filter.test(e)){
System.out.println(e.getName());
}
}
}

上面的例子中:

  1. <? extends Employee> 表示泛型 类型为 Employee 类的对象,或者 Employee 类子类的对象都可。
  2. <? super Employee> 表示传入的对象可以是 Employee 类的对象或者 Employee 类超类的对象,通常,使用该写法来 处理泛型类的函数接口的参数。

通配符也可以不添加边界变量,例子如下:

1
2
3
4
5
6
public static boolean hasNulls(ArrayList<?> elements){
for(Object e : elements){
if(e == null) return true;
}
return false;
}

可以看到,上面的例子中直接使用通配符 ? 表示 ArrayList 可以使用任意的类型参数。实际上,这等价于 public static <T> boolean hasNulls(ArrayList<T> elements). 但是使用通配符书写更加简洁直观。

需要注意的是, 通配符可以作为 类型变量的参数,但是不能作为类型值,i.e.

1
2
Arraylist<?> elements; 
? temp = elements.get(i); //错误!!

这时,我们可以利用上面的 使用泛型符号 T 作为变量的方法 为帮助方法,来帮助 使用通配符 进行表达。

c. 结合使用

更进一步的,在声明函数的过程中,我们可以使用通配符配合类型变量 来编写更加通用的函数,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 例1-子类 + 类型变量
// 调用时,可以向其中加入 E 类的对象或者 E 类子类的对象
public boolean addAll(Collection<? extends E> c){
...
}
// 例2-超类 + 类型变量
public static <T> void printAll(T[] Elements, Predicate<? super T> filter){
for(T e : elements){
if(filter.test(e))
System.out.println(e.toString());
}
}
// 例3-更加综合的例子
public static <T extends Comparable<? super T>> void sort(List<T> list){
...
}

对于例子3,如果写成 <T extends Comparable<T>>, 是否可以呢? 我们 设 Employee 类实现了 Comparable<Employee> 接口,通过比较职员的工资来比较职员。然后, Manager 类继承了 Employee 类,同样实现 Comparable<Employee> 接口,因此,如果上面的 T 为 Manager,则 不正确,因为 Manager 类并不是 Comparable<Manager> 的子类型。所以,必须使用 例子3 中的表示方法。

B. 类型擦除机制

由于兼容性的问题,Java虚拟机 中会对泛型中的类型参数进行擦除(erases)(泛型概念是后期才引入 Java 中的,早期的集合类例如 ArrayList 没有泛型参数)。

具体的,编译器会直接将泛型类中的 类型变量以 raw type 替代。对于一般的变量,使用 Object 替代。而 对于带有通配符规定上限的变量,则使用第一个上限变量替代。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 对于 Entry<K, V> 编译后,K,V 都被替代为 Object
public class Entry{
private Object key;
private Object value;
public Object getKey(){return key;}
public Object getValue(){return value;}
}

// 对于 带有上限的类型变量,
public class Entry<K extends Comparable<? super K> & Serializable,
V extends Serializable>
// 擦除后,使用第一个上限类型替代
public class Entry{
private Comparable key;
private Serializable value;
}

当然,为了保证程序的安全,编译会在编译之前会进行类型的检查- 例如在进行对象构造的时候检查传入的参数是否为对应类型。

此外,在读取被 擦除 类型信息的 变量时,编译器会添加 强制转换,保证运行时安全,例子如下:

1
2
Entry<String, Integer> entry = ...;
String key = (String)entry.getKey(); //编译器会添加强制类型转换

a. 桥接方法

首先看如下例子:

1
2
3
4
5
6
7
8
9
10
11
// 类声明
public calss WordList extends ArrayList<String>{
public boolean add(String e){
return isBadWord(e) ? false : super.add(e);
}
//...
}
// 调用
WordList words = ...;
ArrayList<String> strings = words;
strings.add("C++");

对于上面的调用,由于 Java 的类型擦除机制,调用 strings.add() 时实际上会调用 ArrayListadd 方法(添加一个 Object 类型的变量)。因为Java中重写超类方法时,传入的参数要与超类相同(ref:chap4),因此编译器会在 WordList 类的声明 中添加一个 桥接方法,来连接到 WordList 的 add 方法。具体代码如下:

1
2
3
4
// compiler 向 WordList 类中集成的桥接方法
public boolean add(Object e){
return add((String) e);
}

上述代码可见,通过添加一个桥接方法,重写了 超类中的 add 方法,并且连接到了所声明的类 WordList 中的 add 方法(后面的 add, 等价于 this.add, 参数中,进行了数据类型的强制转换,将 Object 转为 String)。

b. 泛型使用时的注意事项

由于类型擦除机制, Java 的泛型使用有许多的规定和限制。

1: 不允许使用基本数据类型

由上面解释可知,在虚拟机中,所有的泛型类都变为 raw ArrayList, 类型变量都被 Object 替代,但是 基本数据类型不是 Object.

2: 运行时,所有的类型都是raw类型

因此,不能使用 a instanceof ArrayLIst<String>;

强制数据类型转换会报 warning:ArrayList<List> list = (ArrayList<String>) result;,(as 这个转换没有意义); 任意泛型类型使用 getClass() 得到的都是 ArrayList.class

3: 不允许使用类型变量来实例化变量

例如: T[] result = new T[n];,报 error。

通常,上述问题的解决方法是让调用者 直接传入需要的对象,或者传入相应的构造器 (参考下方代码), 或者利用 反射机制,让调用者传入 class 对象。(e.g. String.class

1
2
3
4
5
6
7
8
// 函数实现
public static <T> T[] repeat(int n, T obj, IntFunction<T[]> constr){
T[] result = constr.apply(n);
for(int i = 0; i < n; i++) result[i] = obj;
return result;
}
// 调用
String[] greetings = Arrays.repeat(10, "Hi", String[]::new);

需要注意的是,我们可以实例化带有类型变量参数的 ArrayList, e.g. ArrayList<T> result = new ArrayList<>();

4: 不允许声明对象类型为参数化了的泛型数组

e.g. Entry<String, Integer>[] entries = new Entry<String, Integer>[100]; 会报错。

使用 ArrayList<Entry<String, Integer>> entries = new ArrayList<>(100);

5: 不允许在静态方法或变量中使用类型参数

例如:private static V defaultValue; 会报 error。

6: 注意类型擦除后可能引发的冲突

参考下面例子:

1
2
3
4
5
6
public interface Ordered<T> extends Comparable<T>{
public default boolean equals(T value){
// 会报 error,与 Object.equals 冲突。
return compareTo(value) == 0;
}
}

上面的接口经过类型擦除后,函数变为 equals(Object value)Object 类的相同方法冲突。

7: 不允许抛出或者 catch 泛型类,泛型类不能作为 Throwable 的子类

看例子:

1
2
public class Problem<T> extends Exception; 
// Error. 泛型类不能成为 throwable 类的子类。

但注意, T 作为一个类似通配符的东西,可以放在 throws 的声明中, 参考下面例子:

1
2
3
4
5
6
7
8
public static <V, T extends Throwable> V doWork(Callable<V> c, T ex) throws T{
try{
return c.call();
} catch(Throwable realEx){
ex.initCause(realEx);
throw ex;
}
}