Java-6-泛型编程
在一些情况下,我们的 类 或者 方法 需要能够处理多种 数据类型,例如 数组列表 ArrayList<T>
,可以存储任意类型的元素 T。对于 java, 由于泛型概念为后期添加的内容,为了实现与 早期 java 版本的兼容,java 的泛型编程有很多特殊的限制。
A. 声明与调用
通过例子来看 泛型类 的声明和调用:
1 | // 类的内容 |
从上面的例子中,我们可以看到,我们 写 泛型类的时候,提供 K, V
作为类型参数, 让它们 以类似形参的 形式 传入 变量的类型,最终在实例化的 时候,再具体指明 所处理的参数变量 类型。
实例化时的构造函数可以 省略 尖括号中的内容, Java 将会根据前面的 声明类型自动推断。 (<>
bracket pair 也叫做 diamond )
类似泛型类,泛型方法 同样在方法声明时引入类型参数。泛型方法可以出现在泛型类或者普通类中。 同样我们通过一个例子来说明:
1 | // 在普通类 中 的泛型方法 |
需要注意的是,泛型类和泛型方法 中的类型参数 不允许 是 基本数据类型(可以使用基本数据类型的包装类)。
B. 类型的边界与通配符
a. 构建泛型类/方法时
在构建泛型类或泛型方法的时候,我们可以通过规定泛型类/函数中的类型参数继承某个类或者实现某个接口,来对传入的类型进行 限制(type bounds)。看一个例子:
1 | public static <T extends AutoCloseable> void closeAll(ArrayList<T> elems) |
上面的 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 | // 子类通配符的例子 |
上面的例子中:
<? extends Employee>
表示泛型 类型为 Employee 类的对象,或者 Employee 类子类的对象都可。<? super Employee>
表示传入的对象可以是 Employee 类的对象或者 Employee 类超类的对象,通常,使用该写法来 处理泛型类的函数接口的参数。
通配符也可以不添加边界变量,例子如下:
1 | public static boolean hasNulls(ArrayList<?> elements){ |
可以看到,上面的例子中直接使用通配符 ?
表示 ArrayList 可以使用任意的类型参数。实际上,这等价于 public static <T> boolean hasNulls(ArrayList<T> elements)
. 但是使用通配符书写更加简洁直观。
需要注意的是, 通配符可以作为 类型变量的参数,但是不能作为类型值,i.e.
1 | Arraylist<?> elements; |
这时,我们可以利用上面的 使用泛型符号 T
作为变量的方法 为帮助方法,来帮助 使用通配符 进行表达。
c. 结合使用
更进一步的,在声明函数的过程中,我们可以使用通配符配合类型变量 来编写更加通用的函数,例子如下:
1 | // 例1-子类 + 类型变量 |
对于例子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 | // 对于 Entry<K, V> 编译后,K,V 都被替代为 Object |
当然,为了保证程序的安全,编译会在编译之前会进行类型的检查- 例如在进行对象构造的时候检查传入的参数是否为对应类型。
此外,在读取被 擦除 类型信息的 变量时,编译器会添加 强制转换,保证运行时安全,例子如下:
1 | Entry<String, Integer> entry = ...; |
a. 桥接方法
首先看如下例子:
1 | // 类声明 |
对于上面的调用,由于 Java 的类型擦除机制,调用 strings.add()
时实际上会调用 ArrayList
的 add
方法(添加一个 Object
类型的变量)。因为Java中重写超类方法时,传入的参数要与超类相同(ref:chap4),因此编译器会在 WordList
类的声明 中添加一个 桥接方法,来连接到 WordList
的 add 方法。具体代码如下:
1 | // compiler 向 WordList 类中集成的桥接方法 |
上述代码可见,通过添加一个桥接方法,重写了 超类中的 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 | // 函数实现 |
需要注意的是,我们可以实例化带有类型变量参数的 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 | public interface Ordered<T> extends Comparable<T>{ |
上面的接口经过类型擦除后,函数变为 equals(Object value)
与 Object
类的相同方法冲突。
7: 不允许抛出或者 catch
泛型类,泛型类不能作为 Throwable
的子类
看例子:
1 | public class Problem<T> extends Exception; |
但注意, T 作为一个类似通配符的东西,可以放在 throws 的声明中, 参考下面例子:
1 | public static <V, T extends Throwable> V doWork(Callable<V> c, T ex) throws T{ |