7. 泛型程序设计(Generic programming)¶
泛型程序设计 (Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。 例如, 我们并不希望为聚集 String 和 File 对象分别设计不同的类。实际上,也不需要这样做,因为一个 ArrayList 类可以聚集任何类型的对象。
泛型 1 的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。在表面上看来, 泛型很像 C++ 中的模板。 使用泛型机制编写的程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。
7.1. 泛型类¶
一个泛型类( generic class) 就是具有一个或多个类型变量的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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; } } |
小技巧
类型变量 使用大写形式,且比较短, 这是很常见的。在 Java 库中:
使用变量 E 表示集合的元素类型,
K 和 V 分别表示表的关键字与值的类型。
T ( 需要时还可以用临近的字母 U 和 S) 表示“ 任意类型”。
类型变量 ,用尖括号 ( < >) 括起来
警告
区分概念:泛型类 和 类型变量 。
例如: 假设 T 类型变量, 那么 Pair<T> 是泛型类。
7.2. 泛型方法¶
下面这个方法是在普通类中定义的。
1 2 3 4 5 6 7 8 9 10 11 | public class ArrayAlg { public static <T> T getMidParam(T... a) { return a[a.length / 2]; } public static void main(String[] args) { System.out.println(ArrayAlg.<Integer>getMidParam(1, 2, 3, 4)); System.out.println(getMidParam("a", "b", "c")); } } |
泛型方法可以定义在普通类中,也可以定义在泛型类中。
类型变量放在修饰符(这里是 public static) 的后面,返回类型的前面。
当调用一个泛型方法时,在方法名前的尖括号中放人具体的类型:
String middle = ArrayAlg.<String>getMiddle("private", "Q", "Public");在这种情况(实际也是大多数情况)下,方法调用中可以省略 <String> 类型参数。
7.3. 类型变量的限定¶
有时,类或方法需要对类型变量加以约束。
1 2 3 4 5 6 7 8 9 10 11 12 | public static <T> T min(T[] a) { if (a == null || a.length == 0) { return null; } T smallest = a[0]; for (int i = 1; i < a.length; i++) { if (smallest.compareTo(a[i]) > 0) { smallest = a[i]; } } return smallest; } |
这段代码有一个问题。 变量 smallest 类型为 T, 这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?
解决这个问题的方案是将 T 限制为实现了 Comparable 接口(只含一个方法 compareTo 的标准接口)的类。可以通过对类型变量 T 设置限定(bound) 实现这一点: public static <T extends Comparable> T a) ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class ArrayAlg { public static <T extends Comparable> T min(T[] a) { if (a == null || a.length == 0) { return null; } T smallest = a[0]; for (int i = 1; i < a.length; i++) { if (smallest.compareTo(a[i]) > 0) { smallest = a[i]; } } return smallest; } public static void main(String[] args) { Integer[] num = { 1, 2, 3, 4 }; System.out.println(min(num)); String[] strings = { "json", "parse", "java" }; System.out.println(min(strings)); } } |
一个类型变量或通配符可以有多个限定, 例如: < T extends Comparable & Serializable>
警告
在 C++ 中不能对模板参数的类型加以限制。如果程序员用一个不适当的类型实例化一个模板,将会在模板代码中报告一个(通常是含糊不清的)错误消息。
7.4. 泛型代码和虚拟机¶
虚拟机没有泛型类型对象—所有对象都属于普通类。
7.4.1. 类型擦除¶
无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type )。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变量, 并替换为限定类型(无限定的变量用 Object)。
例如, Pair<T> 的原始类型如下所示: (因为 T 是一个无限定的变量, 所以直接用 Object 替换。)
public class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
second = null;
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object newValue) {
first = newValue;
}
public void setSecond(Object newValue) {
second = newValue;
}
}
警告
就这点而言, Java 泛型与 C++ 模板有很大的区别。C++ 中每个模板的实例化产生不同的类型,这一现象称为“ 模板代码膨账”。Java 不存在这个问题的困扰。
7.5. 泛型的约束和局限性¶
用 Java 泛型时需要考虑的一些限制。其中大多数限制都是由类型擦除引起的。
- 不能用基本类型实例化类型参数
因此, 没有 Pair<double>, 只有 Pair<Double>。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double。
- 运行时类型查询只适用于原始类型
所有的类型查询只产生原始类型。
if (a instanceof Pair<T>) // Error
- 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10]; // Error
- 不能实例化类型变量
不能使用像 new T(…,) new T[…] 或 T.class 这样的表达式中的类型变量。
- Varargs 警告
Java 不支持泛型类型的数组
- 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。
private static T singlelnstance; // Error
- 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。
public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwablecatch 子句中不能使用类型变量。
catch (T e) // Error can 't catch type variable不过, 在异常规范中使用类型变量是允许的。
public static <T extends Throwable〉void doWork(T t) throws T
- 注意擦除后的冲突
将 equals 方法添加到 Pair 类,考虑一个 Pair<String>。从概念上讲, 它有两个 equals 方法:
boolean equals(String) // defined in Pai r<T>以及boolean equals(Object) // inherited from Object。但是,方法擦除boolean equals(String)后就是boolean equals(Object),与 Object.equals 方法发生冲突。当然,补救的办法是重新命名引发错误的方法。
备注
Pair<String> stringPair = . .
Pair<Employee〉employeePair = . .
if (stringPair.getClass() == employeePair.getClass()) // they are equal
//其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class
7.6. 继承规则¶
考虑一个类和一个子类, 如 Employee 和 Manager (Manager 是 Employee 的子类)。 Pair<Manager> 是 Pair<Employee> 的一个子类吗?
答案是 “否”。
7.7. 类型通配符¶
通配符限定与类型变量限定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package genericity; public class ExtendsTest { public static void main(String[] args) { Manager manager1 = new Manager("name", 11, "nan", "office"); Manager manager2 = new Manager("eugene", 22, "gender", "ffa"); Pair<Manager> mPair = new Pair<Manager>(manager1, manager2); printStaffInfo(mPair); /* * printStaffInfo(mPair); 出错如下: The method printStaffInfo(Pair<Staff>) in the * type ExtendsTest is not applicable for the arguments (Pair<Manager>) */ printInfo(mPair); printPairInfo(mPair); } private static void printStaffInfo(Pair<Staff> staffs) { System.out.println(staffs.getFirst().getName()); System.out.println(staffs.getSecond().getName()); } private static <T extends Staff> void printInfo(Pair<T> staffs) { System.out.println(staffs.getFirst().getName()); System.out.println(staffs.getSecond().getName()); } private static void printPairInfo(Pair<? extends Staff> staffs) { System.out.println(staffs.getFirst().getName()); System.out.println(staffs.getSecond().getName()); } } |