为什么要使用泛型程序设计
泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所 重用。例如, 我们并不希望为聚集 String 和 File 对象分别设计不同的类。实际上,也不需要 这样做,因为一个 ArrayList 类可以聚集任何类型的对象。这是一个泛型程序设计的实例。
类型参数的好处
在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。ArrayList 类只维护一个 Object 引用的数组:
public class ArrayList // before generic classes { ... private Object[] elementData; public Object get(int i) { . . , } public void add(Object o) { . . . } }
这种方法有两个问题。当获取一个值时必须进行强制类型转换。
ArrayList files = new ArrayList(); String filename = (String) files.get(0);
此外,这里没有错误检査。可以向数组列表中添加任何类的对象。
files,add(new File(". . .");
对于这个调用,编译和运行都不会出错。然而在其他地方,如果将 get 的结果强制类型 转换为 String类型, 就会产生一个错误。
泛型提供了一个更好的解决方案: 类型参数(type parameters)。ArrayList 类有一个类型 参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>();
这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。
谁想成为泛型程序员
实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想 要内置(plugin) 所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下, 做 所有的事情。因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
定义简单泛型类
一个泛型类(generic class) 就是具有一个或多个类型变量的类。下面 是 Pair 类的代码:
public class Pair<T> { private T first; private T second; public Pair() { first = null; second = null; } public PairfT first, T second) { this,first = first; this.second = second; } public T getFirst() { return first; } public T getSecondO { return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(T newValue) { second = newValue; } }
Pair 类引人了一个类型变量 T,用尖括号 ( < >) 括起来,并放在类名的后面。泛型类可 以有多个类型变量。例如, 可以定义 Pair 类,其中第一个域和第二个域使用不同的类型:
public class Pair<T, U> { . . . }
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。例如,
private T first; // uses the type variable
程序清单 8-1 中的程序使用了 Pair 类。静态的 minmax方法遍历了数组并同时计算出最 小值和最大值。它用一个 Pair 对象返回了两个结果。 回想一下 compareTo 方法比较两个字符 串, 如果字符串相同则返回 0 ; 如果按照字典顺序, 第一个字符串比第二个字符串靠前, 就 返回负值, 否则, 返回正值。
//程序清单 8-1 pair1/PairTest1.java package pair1; /** * @version 1.01 2012-01-26 * @author Cay Horstmann */ public class PairTest1 { public static void main(String[] args) { String[] words = { "Mary", "had", "a", "little", "lamb" }; Pair<String> mm = ArrayAlg.minmax(words); System.out.println("min = " + mm.getFirst()); System.out.println("max = " + mm.getSecond()); } } class ArrayAlg { /** * Gets the minimum and maximum of an array of strings. * @param a an array of strings * @return a pair with the min and max value, or null if a is null or empty */ public static Pair<String> minmax(String[] a) { if (a == null || a.length == 0) return null; String min = a[0]; String 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); } }
泛型方法
前面已经介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法。
class ArrayAlg { public static <T> T getMiddle(T... a) { return a[a.length / 2]; } }
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法, 可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static) 的 后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时 ’ 在方法名前的尖括号中放人具体的类型:
String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");
在这种情况(实际也是大多数情况)下,方法调用中可以省略 <String> 类型参数。编译 器有足够的信息能够推断出所调用的方法。
类型变量的限定
有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中 的最小元素:
class ArrayAIg { public static <T> T min(T[] a) // almost correct { 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; } }
泛型的 min方法只能被实现了 Comparable 接口的类(如 String、 LocalDate 等)的数 组调用。由于 Rectangle类没有实现 Comparable 接口, 所以调用 min将会产生一个编译错误。
在程序清单 8-2 的程序中,重新编写了一个泛型方法 minmax。这个方法计算泛型数组的 最大值和最小值, 并返回 Pair<T>。
//程序清单 8-2 pair2/PairTest2.java package pair2; import java.util.*; /** * @version 1.01 2012-01-26 * @author Cay Horstmann */ public class PairTest2 { public static void main(String[] args) { GregorianCalendar[] birthdays = { new GregorianCalendar(1906, Calendar.DECEMBER, 9), // G. Hopper new GregorianCalendar(1815, Calendar.DECEMBER, 10), // A. Lovelace new GregorianCalendar(1903, Calendar.DECEMBER, 3), // J. von Neumann new GregorianCalendar(1910, Calendar.JUNE, 22), // K. Zuse }; Pair<GregorianCalendar> mm = ArrayAlg.minmax(birthdays); System.out.println("min = " + mm.getFirst().getTime()); System.out.println("max = " + mm.getSecond().getTime()); } } 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 value, 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); } }
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type)。原始类型 的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变M, 并替换为限定类型(无 限定的变量用 Object)。
例如, Pair<T> 的原始类型如下所示:
public class Pair { private Object first; private Object second; 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; } }
因为 T 是一个无限定的变量, 所以直接用 Object 替换。
原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。例如, 类 Pair<T> 中的类型变量没有显式的限定, 因此, 原始类型用 Object 替换 T。假定声明了一 个不同的类型。
public class Interval<T extends Comparable & Serializable〉implements Serializable { private T lower; private T upper; public Interval(T first, T second) { if (first.compareTo(second) <= 0) { lower = first; upper = second; } else { lower = second; upper = first; } } }
原始类型 Interval 如下所示:
public class Interval implements Serializable { private Comparable lower; private Coiparable upper; public Interval(Coiparable first, Coiparable second) { . . . } }
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这 个语句序列
Pair<Employee> buddies = . . .;
Employee buddy = buddies.getFirst();
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。
也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
•对原始方法 Pair.getFirst 的调用。
•将返回的 Object 类型强制转换为 Employee类型。
当存取一个泛型域时也要插人强制类型转换。假设 Pair 类的 first 域和 second 域都是公 有的(也许这不是一种好的编程风格,但在 Java中是合法的)。表达式:
Employee buddy = buddies.first;
也会在结果字节码中插人强制类型转换。
翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法
public static <T extends Comparable〉 T nrin(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable=[] a)
注意,类型参数 T 已经被擦除了, 只留下了限定类型 Comparable。
变量 pair 已经声明为类型 Pair<LocalDate>, 并且这个类型只有一个简单的方法叫setSecond, 即 setSecond(Object)。 虚拟机用 pair 引用的对象调用这个方法。这个对象是 Datelnterval 类型的, 因而将会调用 Datelnterval.setSecond(Object)方法。这个方法是合成的桥方法。
总之,需要记住有关 Java 泛型转换的事实:
•虚拟机中没有泛型,只有普通的类和方法。
•所有的类型参数都用它们的限定类型替换。
•桥方法被合成来保持多态。
•为保持类型安全性,必要时插人强制类型转换。
约束与局限性
不能用基本类型实例化类型参数
没有 Pair<double>, 只 有 Pair<Double>。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值。
这的确令人烦恼。但是,这样做与 Java语言中基本类型的独立状态相一致。这并不是一 个致命的缺陷— —只有 8 种基本类型, 当包装器类型(wrapper type) 不能接受替换时, 可以使用独立的类和方法处理它们。
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
例如:
if (a instanceof Pair<String>) // Error
实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:
if (a instanceof Pair<T>) // Error
或强制类型转换:
Pair<St「ing> p = (Pair<String>) a; // Warning-can only test that a is a Pair
为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会 得到一个编译器错误, 如果使用强制类型转换会得到一个警告。
不能创建参数化类型的数组
不能实例化参数化类型的数组, 例如:
Pair<String>[] table = new Pair<String>[10]; // Error
数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个 ArrayStoreException 异常:
objarray[0] = "Hello"; // Error component type is Pair
不过对于泛型类型, 擦除会使这种机制无效。以下赋值:
objarray[0] = new Pair<Employee>0;
能够通过数组存储检査, 不过仍会导致一个类型错误。出于这个原因, 不允许创建参数 化类型的数组。 需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair<String>[] 的变量仍是合法 的。不过不能用
new Pair<String>[10]
初始化这个变量。
Varargs 警告
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解@ SuppressWamings("unchecked")。 或者在 Java SE 7中, 还 可 以 用@SafeVarargs 直 接 标 注 addAll 方法:
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方 法,都可以使用这个注解,这仅限于最常见的用例。
不能实例化类型变置
不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的 Pair<T> 构造器就是非法的:
public Pair() { first = new T(); second = new T(); } // Error
类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object()。在 Java SE 8 之后, 最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p = Pair.makePairCString::new);
makePair方法接收一个 Supplier<T>,这是一个函数式接口,表示一个无参数而且返回 类型为 T 的函数:
public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get0.constr.get0); }
比较传统的解决方法是通过反射调用 Clasmewlnstance 方法来构造泛型对象。
不能构造泛型数组
类型擦除会让这个方法永远构造 Comparable[2] 数组。 如果数组仅仅作为一个类的私有实例域, 就可以将这个数组声明为 Object[],并且在获 取元素时进行类型转换。例如’ ArrayList 类可以这样实现:
public class ArrayList<E> { private Object口 elements; ... @SuppressWarnings("unchecked") public E get(int n) { return (E) elements[n]; } public void set(int n, E e) { elements[n] = e; } // no cast needed }
实际的实现没有这么清晰:
public class ArrayList<E> { private E[] elements; ... public ArrayList() { elements = (ED) new Object[10]; } }
这里, 强制类型转换 E[ ] 是一个假象, 而类型擦除使其无法察觉。
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如, 下列高招将无法施展:
public class Singleton<T> { private static T singlelnstance; // Error public static T getSinglelnstanceO // Error { if (singleinstance == null) construct new instanceof T return singlelnstance; } }
如果这个程序能够运行, 就可以声明一个 Singleton<Random> 共享随机数生成器, 声明 一个 Singlet0n<JFileCh00Ser> 共享文件选择器对话框。但是, 这个程序无法工作。类型擦除 之后, 只剩下 Singleton类,它只包含一个 singlelnstance 域。 因此, 禁止使用带有类型变量 的静态域和方法。
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。 例如, 以下定义就不能正常编译:
public class Problem<T> extends Exception { /* ...*/ } // Error can't extend Throwable
catch 子句中不能使用类型变量。
可以消除对受查异常的检查
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用 泛型消除这个限制。关键在于以下方法:
@SuppressWamings("unchecked")
public static <T extends Throwable〉void throwAs(Throwable e) throws T {
throw (T) e;
}
假设这个方法包含在类 Block 中, 如果调用
Block.<RuntimeException>throwAs(t);
编译器就会认为 t 是一个非受查异常。
通过使用泛型类、擦除和@SuppressWamings 注解, 就能消除 Java 类型系统的部分基本 限制。
注意擦除后的冲突
当泛型类型被擦除时, 无法创建引发冲突的条件。
下面是一个示例。假定像下面这样将 equals方法添加到 Pair 类中:
public class Pair<T> { public boolean equals(T value) { return first,equals(value) && second,equals(value); } ... }
考虑一个 Pair<String>。从概念上讲, 它有两个 equals 方法:
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object
但是,直觉把我们引入歧途。方法擦除
boolean equals(T) 就是
boolean equals(Object)
与 Object.equals方法发生冲突。
当然,补救的办法是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则: “ 要想支持擦除的转换, 就需要强行限制一个类或类 型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。 ” 例如, 下述代码是非法的:
class Employee implements Coinparab1e<Emp1oyee> { . . . } class Manager extends Employee implements Comparable<Hanager> {... } // Error Manager
会实现Comparable<Employee> 和 Comparable<Manager>, 这是同一接口的不同 参数化。
泛型类型的继承规则
永远可以将参数化类型转换为一个原始类型。例如,PaiKEmployee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要。
必须注意泛型与 Java 数组之间的重要区别。可以将一个 Manager□ 数组賦给一个 类型为 Employee[] 的变量:
Manager口 managerBuddies = { ceo, cfo };
Employee[] employeeBuddies = managerBuddies; // OK
然而,数组带有特别的保护。如果试图将一个低级别的雇员存储到 employeeBuddies[0], 虚拟机将会抛出 ArrayStoreException 异常。
永远可以将参数化类型转换为一个原始类型。例如,PaiKEmployee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要。
浙公网安备 33010602011771号