)));
将 Dictionary<Integer,Component >对象传递给 setLabelTable 时,编译器会发出一个警告。
slider.setLabelTable(labelTable): // Warning
毕竟,编译器无法确定 setLabelTable 可能会对 Dictionary 对象做什么操作。这个方法可 能会用字符串替换所有的关键字。这就打破了关键字类型为整型( Integer) 的承诺,未来的 操作有可能会产生强制类型转换的异常。
这个警告对操作不会产生什么影响, 最多考虑一下 JSlider 有可能用 Dictionary 对象做什 么就可以了。在这里十分清楚,JSlider 只阅读这个信息, 因此可以忽略这个警告。
现在,看一个相反的情形, 由一个遗留的类得到一个原始类型的对象。可以将它赋给一 个参数化的类型变量, 当然,这样做会看到一个警告。 例如:
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // Warning
这就行了。再看一看警告, 确保标签表已经包含了 Integer 和 Component 对象。 当然, 从来也不会有绝对的承诺。恶意的编码者可能会在滑块中设置不同的 Dictionary。然而, 这 种情况并不会比有泛型之前的情况更糟糕。最差的情况就是程序抛出一个异常。
在查看了警告之后,可以利用注解 ( annotation) 使之消失。注释必须放在生成这个警告 的代码所在的方法之前,如下:
@SuppressWarnings("unchecked")
Dictionary<Integer, Components〉labelTable = slider.getLabelTable(); // No warning
或者,可以标注整个方法,如下所示:
@SuppressWarnings("unchecked")
public void configureSlider() { . . . }
这个注解会关闭对方法中所有代码的检査。
8.6 约束与局限性
在下面几节中, 将阐述使用 Java 泛型时需要考虑的一些限制。大多数限制都是由 类型擦除 引起的。
8.6.1 不能用基本类型实例化类型参数
不能用类型参数代替基本类型。因此, 没有 Pair<double>, 只 有 Pair<Double> 。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double 值。
这的确令人烦恼。但是,这样做与 Java 语言中基本类型的独立状态相一致。这并不是一 个致命的缺陷—只有 8 种基本类型, 当包装器类型(wrapper type) 不能接受替换时, 可以使用独立的类和方法处理它们。
8.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。 例如:
if (a instanceof Pair<String>) // Error
实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:
if (a instanceof Pair<T>) // Error
或强制类型转换:
Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair
为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会 得到一个编译器错误, 如果使用强制类型转换会得到一个警告。
同样的道理, getClass 方法总是返回原始类型。例如:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) // they are equal
其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class。
8.6.3 不能创建参数化类型的数组
不能实例化参数化类型的数组, 例如:
Pair<String>[] table = new Pair<String>[10]; // Error
这有什么问题呢? 擦除之后, table 的类型是 Pair[] 。可以把它转换为 Object[] :
Object[] objarray = table;
数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个 ArrayStoreException 异常:
objarray[0] = "Hello"; // Error component type is Pair
不过对于泛型类型, 擦除会使这种机制无效。以下赋值:
objarray[0] = new Pair<Employee>();
能够通过数组存储检査, 不过仍会导致一个类型错误。出于这个原因, 不允许创建参数 化类型的数组。
需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair<String>[] 的变量仍是合法 的。不过不能用 new Pair[10] 初始化这个变量。
注释: 可以声明通配类型的数组, 然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
结果将是不安全的。如果在 table[0] 中存储一个 Pair, 然后对 table[0]. getFirst() 调用一个 String 方法, 会得到一个 ClassCastException 异常。
提示:如果需要收集参数化类型对象, 只有一种安全而有效的方法:使用 ArrayList:ArrayList<Pair<String>>。
8.6.4 Varargs 警告
上一节中已经了解到, Java 不支持泛型类型的数组。这一节中我们再来讨论一个相关的 问题:向参数个数可变的方法传递一个泛型类型的实例。
考虑下面这个简单的方法, 它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll, T... ts)
for (t : ts) coll.add(t);
应该记得,实际上参数 ts 是一个数组, 包含提供的所有实参。
现在考虑以下调用:
Col1ection<Pair<String>> table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 =. . .;
addAll(table, pairl, pair2);
为了调用这个方法,Java 虚拟机必须建立一个 Pair 数组, 这就违反了前面的规 则。不过,对于这种情况, 规则有所放松,你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @ SuppressWarnings("unchecked") 。或者在 Java SE 7中, 还 可 以 用@SafeVarargs 直 接 标 注 addAll 方法:
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方 法,都可以使用这个注解,这仅限于最常见的用例。
注释: 可以使用 @SafeVarargs 标注来消除创建泛型数组的有关限制, 方法如下:
@SafeVarargs static <E> E[] array(E... array) { return array; }
现在可以调用:
Pair<String>[] table = array(pairl,pair2);
这看起来很方便,不过隐藏着危险。以下代码:
Object[] objarray = table;
objarray[0] = new Pair<Employee>();
能顺利运行而不会出现 ArrayStoreException 异常(因为数组存储只会检查擦除的类 型) ,但在处理 table[0] 时你会在别处得到一个异常。
8.6.5 不能实例化类型变置
不能使用像 new T(...), newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的 Pair 构造器就是非法的:
public Pair() { first = new T(); second = new T(); } // Error
类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object() 。在 Java SE 8 之后, 最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p = Pair.makePair(String::new);
makePair 方法接收一个 Supplier<T>,这是一个函数式接口,表示一个无参数而且返回 类型为 T 的函数:
public static <T> Pair<T> makePair(Supplier<T> constr)
return new Pair<>(constr.get(). constr.get());
比较传统的解决方法是通过反射调用 Class.newlnstance 方法来构造泛型对象。
遗憾的是,细节有点复杂。不能调用:
first = T.class.newInstance(); // Error
表达式 T.class 是不合法的, 因为它会擦除为 Object.class。必须像下面这样设计 API 以便得 到一个 Class 对象:
public static <T> Pair<T> makePair(Class<T> cl)
try { return new Pair<>(cl.newInstance(), cl.newInstance());}
catch (Exception ex) { return null; }
}
这个方法可以按照下列方式调用:
Pair<String> p = Pair.makePair(String.class);
注意,Class类本身是泛型。 例如,String.class 是一个 Class <String>的实例(事实上, 它是唯一的实例)。 因此,makePair 方法能够推断出 pair 的类型。
8.6.6 不能构造泛型数组
就像不能实例化一个泛型实例一样, 也不能实例化数组。不过原因有所不同,毕竟数组 会填充 null 值,构造时看上去是安全的。不过, 数组本身也有类型,用来监控存储在虚拟机 中的数组。这个类型会被擦除。 例如,考虑下面的例子:
public static <T extends Comparable> T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // Error
类型擦除会让这个方法永远构造 Comparable[2] 数组。
如果数组仅仅作为一个类的私有实例域, 就可以将这个数组声明为 Object[],并且在获取元素时进行类型转换。例如, ArrayList 类可以这样实现:
public cl ass 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 = (E[]) new Object[10]; }
这里, 强制类型转换 E[ ] 是一个假象, 而类型擦除使其无法察觉。
由于 minmax 方法返回 T[ ] 数组,使得这一技术无法施展, 如果掩盖这个类型会有运行 时错误结果。假设实现代码:
public static <T extends Comparable> T[] minmax(T... a)
Object[] mm = new Object[2];
return (T[]) mm; // compiles with warning
String[] ss = ArrayAlg.minmax("Tom", "Dick", "Harry");
编译时不会有任何警告。当 Object[] 引用赋给 Comparable[] 变量时,将会发生 ClassCastException 异常。
在这种情况下, 最好让用户提供一个数组构造器表达式:
String[] ss = ArrayAlg.minmax (String[]::new,"Tom", "Dick", "Harry");
构造器表达式 String::new 指示一个函数, 给定所需的长度, 会构造一个指定长度的 String 数组。
minmax 方法使用这个参数生成一个有正确类型的数组:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
T[] mm = constr.apply(2);
比较老式的方法是利用反射, 调用 Array.newlnstance:
public static <T extends Comparable> T[] minmax(T... a)
T[] mm = (T[]) Array.newlnstance (a.getClass().getComponentType() , 2);
ArrayList 类的 toArray 方法就没有这么幸运。它需要生成一个 T[] 数组, 但没有成分类型。因此, 有下面两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大, 就使用这个数组。 否则, 用 result 的 成分类型构造一个足够大的新数组。
8.6.7 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如, 下列高招将无法施展:
public class Singleton<T>
private static T singlelnstance; // Error
public static T getSinglelnstance() // Error
if (singleinstance == null) construct new instance of T
return singlelnstance;
如果这个程序能够运行, 就可以声明一个 Singleton 共享随机数生成器, 声明 一个 Singleton<JFileChooser> 共享文件选择器对话框。但是, 这个程序无法工作。类型擦除 之后, 只剩下 Singleton 类,它只包含一个 singlelnstance 域。 因此, 禁止使用带有类型变量 的静态域和方法。
8.6.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。 例如, 以下定义就不能正常编译:
public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable
catch 子句中不能使用类型变量。例如, 以下方法将不能编译:
public static <T extends Throwable> void doWork(Class<T> t)
do work
catch (T e) // Error can 't catch type variable
Logger.global.info(...)
不过, 在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T // OK
do work
catch (Throwable realCause)
t.initCause(realCause);
throw t;
8.6.9 可以消除对受查异常的检查
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于以下方法:
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T
throw (T) e;
假设这个方法包含在类 Block 中, 如果调用
Block.<RuntimeException>throwAs(t);
编译器就会认为 t 是一个非受查异常。 以下代码会把所有异常都转换为编译器所认为的 非受查异常:
do work
catch (Throwable t)
B1ock.<RuntimeException>throwAs(t) ;
下面把这个代码包装在一个抽象类中。用户可以覆盖 body 方法来提供一个具体的动作。 调用 toThread 时, 会得到 Thread 类的一个对象, 它的 run 方法不会介意受查异常。
public abstract class Block
public abstract void body() throws Exception;
public Thread toThread()
return new Thread()
public void run()
body();
catch (Throwable t)
B1ock.<RuntimeException>throwAs(t);
@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T
throw (T) e;
例如, 以下程序运行了一个线程, 它会拋出一个受查异常。
public class Test
public static void main(String[] args)
new Block()
public void body() throws Exception
Scanner in = new Scanner(new File("ququx") , "UTF-8");
while (in.hasNext())
System.out.println (in.next());
.toThread() .start();
运行这个程序时, 会得到一个栈轨迹, 其中包含一个 FileNotFoundException ( 当然, 假 设你没有提供一个名为 ququx 的文件)。
这有什么意义呢? 正常情况下, 你必须捕获线程 run 方法中的所有受查异常, 把它们 “ 包装” 到非受查异常中, 因为 run 方法声明为不抛出任何受查异常。
不过在这里并没有做这种“ 包装”。我们只是抛出异常, 并“ 哄骗” 编译器, 让它认为 这不是一个受查异常。 通过使用泛型类、 擦除和 @SuppressWarnings 注解, 就能消除 Java 类型系统的部分基本 限制。
8.6.10 注意擦除后的冲突
当泛型类型被擦除时, 无法创建引发冲突的条件。下面是一个示例。假定像下面这样将 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 Comparab1e<Employee> { . . . }
class Manager extends Employee implements Comparable<Manager>
{ . . . } // Error
Manager 会实现 Comparable 和 Comparable, 这是同一接口的不同 参数化。
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的。
class Employee implements Comparable { . . . }
class Manager extends Employee implements Comparable { . . . }
其原因非常微妙, 有可能与合成的桥方法产生冲突。实现了 Comparable<X> 的类可以获得一 个桥方法
public int compareTo(Object other) { return compareTo((X) other); }
对于不同类型的 X 不能有两个这样的方法。
8.7 泛型类型的继承规则
在使用泛型类时,需要了解一些有关继承和子类型的准则。下面先从许多程序员感觉不 太直观的情况开始。考虑一个类和一个子类, 如 Employee 和 Manager。Pair 是 Pair<Employee> 的一个子类吗? 答案是“ 不是”, 或许人们会感到奇怪。例如, 下面的代码 将不能编译成功:
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.ininmax (topHonchos) ; // Error
minmax 方法返回 Pair<Manager>, 而不是 Pair<Employee>,并且这样的赋值是不合法的。
无论 S 与 T 有什么联系 (如图 8-1 所示,) 通常, Pair<S> 与 Pair<T>没有什么联系。
这一限制看起来过于严格, 但对于类型安全非常必要。假设允许将 Pair<Manager>转换 为 Pair<Employee>。考虑下面代码:
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies; // illegal , but suppose it wasn't
employeeBuddies.setFirst(1owlyEmployee) ;
显然, 最后一句是合法的。但是 employeeBuddies 和 managerBuddies 引用了同样的对 象。现在将 CFO 和 一 个 普 通 员 工 组 成 一 对,这 对 于 说 应 该 是 不 可 能 的。
注释: 必须注意泛型与 Java 数组之间的重要区别。可以将一个 Manager[] 数组賦给一个 类型为 Employee[] 的变量:
Manager[] managerBuddies = { ceo, cfo };
Employee[] employeeBuddies = managerBuddies; // OK
然而,数组带有特别的保护。如果试图将一个低级别的雇员存储到 employeeBuddies[0] ,虚拟机将会抛出 ArrayStoreException 异常。
永远可以将参数化类型转换为一个原始类型。例如,Pair<Employee> 是原始类型 Pair 的 一个子类型。在与遗留代码衔接时,这个转换非常必要。
转换成原始类型之后会产生类型错误吗? 很遗憾, 会! 看一看下面这个示例:
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File(". . .")); // only a compile-time warning
听起来有点吓人。但是, 请记住现在的状况不会再比旧版 Java 的情况糟糕。虚拟机的安 全性还没有到生死攸关的程度。当使用 getFirst 获得外来对象并赋给 Manager 变量时, 与通 常一样, 会抛出 ClassCastException 异常。这里失去的只是泛型程序设计提供的附加安全性。
最后, 泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么 区别。例如, ArrayList<T> 类实现 List<T> 接口。这意味着, 一个 ArrayList<Manager> 可 以被转换为一个 List<Manager>。但是, 如前面所见, 一个 ArrayList<Manager> 不是一个 ArrayList<Employee> 或 List<Employee> 。图 8-2 展示了它们之间的联系。
8.8 通配符类型
固定的泛型类型系统使用起来并没有那么令人愉快, 类型系统的研究人员知道这一点已经有一段时间了。Java 的设计者发明了一种巧妙的(仍然是安全的) “解决方案”:通配符类型。 下面几小节会介绍如何处理通配符。
8.8.1 通配符概念
通配符类型中, 允许类型参数变化。 例如, 通配符类型
Pair<? extends Employee>
表示任何泛型 Pair 类型, 它的类型参数是 Employee 的子类, 如 Pair<Manager>, 但不是 Pair<String>。
假设要编写一个打印雇员对的方法, 像这样:
public static void printBuddies(Pair<Employee> p)
Employee first = p.getFirst();
Employee second = p.getSecond();
Systefn.out.println(first.getName() + " and " + second.getName() + " are buddies.");
正如前面讲到的,不能将 Pair<Manager> 传递给这个方法,这一点很受限制。解决的方 法很简单:使用通配符类型:
public static void printBuddies(Pair<? extends Employee> p)
类型 Pair<Manager> 是 Pair<? extends Employee> 的子类型(如图 8-3 所示)。
使用通配符会通过 Pair<? extends Employee> 的引用破坏 Pair<Manager>吗?
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wi1dcardBuddies.setFirst(1owlyEnployee); // compile-time error
这可能不会引起破坏。对 setFirst 的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型 Pair<? extends Employee>。其方法似乎是这样的:
? extends Employee getFirst()
void setFirst(? extends Employee)
这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。
使用 getFirst 就不存在这个问题: 将 getFirst 的返回值赋给一个 Employee 的引用完全合法。 这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。
8.8.2 通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定 (supertype bound), 如下所示:
? super Manager
这个通配符限制为 Manager 的所有超类型。(已有的 super 关键字十分准确地描述了这种 联系, 这一点令人感到非常欣慰。)
为什么要这样做呢? 带有超类型限定的通配符的行为与 8.8 节介绍的相反。可以为方法 提供参数, 但不能使用返回值。例如, Pair<? super Manager> 有方法
void setFirst(? super Manager)
? super Manager getFirst()
这不是真正的 Java 语法,但是可以看出编译器知道什么。编译器无法知道 setFirst 方法 的具体类型, 因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。 只能传递 Manager 类型的对象,或者某个子类型(如 Executive) 对象。另外, 如果调用 getFirst, 不能 保证返回对象的类型。只能把它赋给一个 Object。
下面是一个典型的示例。有一个经理的数组,并且想把奖金最高和最低的经理放在一个 Pair 对象中。Pair 的类型是什么? 在这里,Pair<Employee> 是合理的, Pair<Object> 也是合理的(如图 8-4所示)。下面的方法将可以接受任何适当的 Pair:
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result)
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for (int i *1; i < a.length; i++)
if (min.getBonus() > a[i].getBonus()) min = a[i];
if (max.getBonus() < a[i].getBonus()) max = a[i];
result.setFirst(min);
result.setSecond(max);
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
下面是超类型限定的另一种应用。Comparable 接口本身就是一个泛型类型。声明如下:
public interface Comparable<T>
public int compareTo(T other);
在此,类型变量指示了 other 参数的类型。例如, String类实现 Comparable<String> , 它 的 compareTo 方法被声明为
public int compareTo(String other)
很好,显式的参数有一个正确的类型。接口是一个泛型接口之前,other 是一个 Object, 并且这个方法的实现需要强制类型转换。
由于 Comparable 是一个泛型类型, 也许可以把 ArrayAIg类的 min方法做得更好一些? 可以这样声明:
public static <T extends Comparable<T>> T min(T[] a)
看起来, 这样写比只使用 T extents Comparable 更彻底, 并且对许多类来讲, 工作 得更好。 例如, 如果计算一个 String 数组的最小值, T 就是 String类型的, 而 String 是 Comparable<String> 的子类型。但是, 处理一个 LocalDate 对象的数组时, 会出现一个问题。 LocalDate 实现了 ChronoLocalDate, 而 ChronoLocalDate 扩展了 Comparable<ChronoLocalDate> 。 因此, LocalDate 实现的是 Comparable<ChronoLocalDate> 而不是 Comparable<LocalDate>。
在这种情况下, 超类型可以用来进行救助:
public static <T extends Conparable<? super T>> T min(T[] a) ...
现在 compareTo 方法写成
int compareTo(? super T)
有可能被声明为使用类型 T 的对象, 也有可能使用 T 的超类型(如当 T 是 LocalDate, T 的一个子类型) 。无论如何,传递一个 T 类型的对象给 compareTo 方法都是安全的。
对于初学者来说,<T extends Comparable<? super T>>这样的声明看起来有点吓人。很遗憾, 因为这一声明的意图在于帮助应用程序员排除调用参数上的不必要的限制。对泛型没有兴趣的应用程序员很可能很快就学会掩盖这些声明,想当然地认为库程序员做的都是正确的。如果是一名库程序员,一定要习惯于通配符,否则,就会受到用户的责备,还要在代码中随意地添加强制类型转换直至代码可以编译。
注 释: 子 类 型 限 定 的 另 一 个 常 见 的 用 法 是 作 为 一 个 函 数 式 接 口 的 参 数 类 型。 例 如,Collection 接口有一个方法:
default boolean removeIf(Predicated<? super E> filter)
这个方法会删除所有满足给定谓词条件的元素。例如, 如果你不喜欢有奇怪散列码 的员工,就可以如下将他们删除:
ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() %2 != 0;
staff.removelf(oddHashCode):
你希望传入一个 Predicate<Object> , 而不只是 Predicate<Employee>。Super 通配符可 以使这个愿望成真。
8.8.3 无限定通配符
还可以使用无限定的通配符, 例如,Pair<?> 。初看起来,这好像与原始的 Pair 类型一样。 实际上, 有很大的不同。类型 Pair<?>有以下方法:
? getFirst()
void setFirst(?)
getFirst 的返回值只能赋给一个 Object。setFirst 方法不能被调用, 甚至不能用 Object 调 用。Pair<?> 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。
注释: 可以调用 setFirst(null)。
为什么要使用这样脆弱的类型? 它对于许多简单的操作非常有用。例如,下面这个方法 将用来测试一个 pair 是否包含一个 null 引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p)
return p.getFirst() = null || p.getSecond() =null;
通过将 hasNulls 转换成泛型方法,可以避免使用通配符类型:
public static <T> boolean hasNulls(Pair<T> p)
但是,带有通配符的版本可读性更强。
8.8.4 通配符捕获
编写一个交换成对元素的方法:
public static void swap(Pair<?> p)
通配符不是类型变量, 因此, 不能在编写代码中使用“ ?” 作为一种类型。 也就是说, 下述 代码是非法的:
? t = p.getFirst(); // Error
p.setFirst(p.getSecond());
p.setSecond(t);
这是一个问题, 因为在交换的时候必须临时保存第一个元素。幸运的是, 这个问题有一 个有趣的解决方案。我们可以写一个辅助方法 swapHelper, 如下所示:
public static <T> void swapHelper(Pair<T> p)
T t = p.getFirst();
p.setFirst(p. getSecond());
p.setSecond(t);
注意, swapHelper 是一个泛型方法, 而 swap 不是, 它具有固定的 Pair<?> 类型的参数。
现在可以由 swap 调用 swapHelper:
public static void swap(Pair<?> p) { swapHelper(p); }
在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道是哪种类型的通配符, 但是, 这是一个明确的类型,并且 swapHelper 的定义只有在 T 指出类型时才有明确的含义。
当然,在这种情况下, 并不是一定要使用通配符。 我们已经直接实现了没有通配符的泛型方法<T> void swap(Pair<T> p)。 然而,下面看一个通配符类型出现在计算中间的示例:
public static void maxminBonus(Manager [] a, Pair<? super Manager> result)
minmaxBonus(a, result);
PairAlg.swap(result); // OK swapHelper captures wildcard type
在这里, 通配符捕获机制是不可避免的。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、 确定的类型。 例如, ArrayList<pair<T>> 中的 T 永远不能捕获 ArrayList<pair<?>> 中的通配符。数组列表可以保存两个pair<?>, 分别针对?的不同类型。
程序清单 8-3 中的测试程序将前几节讨论的各种方法综合在一起, 读者从中可以看到它 们彼此之间的关联。(略过)
8.9 反射和泛型
反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。在下面的小节中,可以了解利用反射可以获得泛型类 的什么信息。
8.9.1 泛型Class类
现在, Class 类是泛型的。例如, String.class 实际上是一个Class <String>类的对象(事实上,是唯一的对象) 。
类型参数十分有用, 这是因为它允许 Class<T> 方法的返回类型更加具有针对性。下面 Class<T>中的方法就使用了类型参数:
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclass()
Constructor<T> getConstructor(C1ass... parameterTypes)
Constructor<T> getDeclaredConstructor(Class... parameterTypes)
newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型 目前被声明为 T, 其类型与 Class<T> 描述的类相同,这样就免除了类型转换。
如果给定的类型确实是 T 的一个子类型,cast 方法就会返回一个现在声明为类型 T 的对 象, 否则,抛出一个 BadCastException 异常。
如果这个类不是 enum 类或类型 T 的枚举值的数组, getEnumConstants 方法将返回 null。
最后, getConstructor 与 getdeclaredConstructor 方 法 返 回 一 个 Constructor<T> 对象。 Constructor 类也已经变成泛型, 以便 newlnstance 方法有一个正确的返回类型。
API java.lang.Class<T> 1.0
• T newInstance()
返回无参数构造器构造的一个新实例。
• T cast(Object obj)
如果 obj 为 null 或有可能转换成类型 T, 则 返 回 obj ; 否 则 拋 出 BadCastException 异常。
• T[ ] getEnumConstants( ) 5.0
如果 T 是枚举类型, 则返回所有值组成的数组,否则返回 null。
• Class getSuperclass( )
返回这个类的超类。如果 T 不是一个类或 Object 类, 则返回 null。
• Constructor getConstructor(Class... parameterTypes) 1.1
• Constructor getDeclaredConstructor(Class... parameterTypes) 1.1
获得公有的构造器, 或带有给定参数类型的构造器。
API java.lang.reflect.Constructor<T> 1.1
• T newlnstance(object. . . parameters)
返回用指定参数构造的新实例。
8.9.2 使用 Class 参数进行类型匹配
有时, 匹配泛型方法中的 Class 参数的类型变量很有实用价值。下面是一 标准的示例:
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,
IllegalAccessException
return new Pair<>(c.newInstance(), c.newInstance());
makePair(Employee.class)
Employee.class 是类型 Class<Employee> 的一个对象。makePair 方法的类型参数 T 同 Employee 匹配, 并且编译器可以推断出这个方法将返回一个 Pair <Employee>。
8.9.3 虚拟机中的泛型类型信息
Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是, 擦除的类 仍然保留一些泛型祖先的微弱记忆。例如, 原始的 Pair 类知道源于泛型类 Pair<T>, 即使一 个 Pair 类型的对象无法区分是由 Pair<String> 构造的还是由 Pair<Employee> 构造的。
类似地, 看一下方法
public static Comparable min(Comparable[] a)
这是一个泛型方法的擦除
public static <T extends Comparable<? super T>> T min(T[] a)
可以使用反射 API 来确定:
• 这个泛型方法有一个叫做 T 的类型参数。
• 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
• 这个限定类型有一个通配符参数。
• 这个通配符参数有一个超类型限定。
• 这个泛型方法有一个泛型数组参数。
换句话说,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道 对于特定的对象或方法调用,如何解释类型参数。
为了表达泛型类型声明,使用java.lang.reflect 包中提供的接口 Type。这个接口包含下列 子类型:
• Class 类,描述具体类型。
• TypeVariable 接口,描述类型变量(如 T extends Comparable<? super T>) 。
• WildcardType 接口, 描述通配符 (如 ?super T)。
• ParameterizedType 接口, 描述泛型类或接口类型(如 Comparable<? super T> )。
• GenericArrayType 接口,描述泛型数组(如 T[ ])。
图 8-5 给出了继承层次。注意, 最后 4 个子类型是接口, 虚拟机将实例化实现这些接口 的适当的类。
程序清单 8-4 中使用泛型反射 API 打印出给定类的有关内容。如果用Pair 类运行, 将会 得到下列报告:
class Pair<T> extends java.lang.Object
public T getFirst()
public T getSecond()
public void setFirst(T)
public void setSecond(T)
如果使用 PairTest2 目录下的 ArrayAlg 运行, 将会得到下列报告:(略过)
public static <T extends java.lang.Comparable> Pair<T> minmax (T[])
本节末尾的 API 注释描述了示例程序中使用的这些方法。
API java.lang.Class<T> 1.0
• TypeVariable[ ] getTypeParameters( ) 5.0
如果这个类型被声明为泛型类型,则获得泛型类型变量,否则获得一个长度为 0 的数组。
• Type getGenericSuperclass( ) 5.0
获得被声明为这一类型的超类的泛型类型;如果这个类型是 Object 或不是一个类类型 ( class type), 则返回 null。
• Type[ ] getGenericInterfaces( ) 5.0
获得被声明为这个类型的接口的泛型类型(以声明的次序) ,否则, 如果这个类型没有 实现接口,返回长度为 0 的数组。
API java.lang.reflect.Method 1.1
• TypeVariable[ ] getTypeParameters( ) 5.0
如果这个方法被声明为泛型方法, 则获得泛型类型变量,否则返回长度为 0 的数组。
• Type getGenericReturnType( ) 5.0
获得这个方法被声明的泛型返回类型。
• Type[ ] getGenericParameterTypes( ) 5.0
获得这个方法被声明的泛型参数类型。 如果这个方法没有参数,返回长度为 0 的 数组。
API java.lang.reflect.TypeVariable 5.0
• String getName( )
获得类型变量的名字。
• Type[ ] getBounds( )
获得类型变量的子类限定,否则,如果该变量无限定, 则返回长度为 0 的数组。
API java.lang.reflect.WildcardType 5.0
• Type[ ] getUpperBounds( )
获得这个类型变量的子类 ( extends) 限定,否则, 如果没有子类限定,则返回长度为 0 的数组。
• Type[ ] getLowerBounds( )
获得这个类型变量的超类(super) 限定,否则, 如果没有超类限定,则返回长度为 0 的数组。
API java.lang.reflect.ParameterizedType 5.0
• Type getRawType( )
获得这个参数化类型的原始类型。
• Type[ ] getActualTypeArguments( )
获得这个参数化类型声明时所使用的类型参数。
• Type getOwnerType( )
如果是内部类型, 则返回其外部类型,如果是一个顶级类型, 则返回 null。
API java.lang.reflect.GenericArrayType 5.0
• Type getGenericComponentType( )
获得声明该数组类型的泛型组件类型。
现在已经学习了如何使用泛型类以及在必要时如何自定义泛型类和泛型方法。同样重要的是,学习了如何解译在 API 文档和错误消息中遇到的泛型类型声明。要想了解有关 Java 泛 型更加详尽的信息,可以到 http://angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html 上求 助。那里有一个很好的常见问题解答列表(也有一些不太常见的问题。)
在下一章中,将学习Java 集合框架如何使用泛型。
恭喜,本章完!