第8章 泛型程序设计
8.1 为什么要使用泛型程序设计
泛型程序设计
- 泛型程序设计 (Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。
8.1.1 类型参数的好处
public class ArrayList { // before generic classes
private Object[] elementData;
...
public Object get(int i) { . . , }
public void add(Object o) { . . . }
}
ArrayList files = new ArrayListO;
String filename = (String) files.get(O);
files.add(new File("..."));
存在问题
- 当获取一个值时必须进行强制类型转换。
- 此外,这里没有错误检査。可以向数组列表中添加任何类的对象。
解决办法
- 泛型提供了一个更好的解决方案 : 类型参数 (type parameters)。
ArrayList 类有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>():
省略泛型类型
- 在 Java SE 7及以后的版本中, 构造函数中可以省略泛型类型:
ArrayList<String> files = new ArrayList<>();
编译器处理
- 编译器可以进行检査,避免插人错误类型的对象。
- 出现编译错误比类在运行时出现类的强制类型转换异常要好得多。
8.2 定义简单泛型类
泛型类
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; }
}
public class Pair<T, U> { . . . }
- 一个泛型类(generic class) 就是具有一个或多个类型变量的类。
- 类型变量 T,用尖括号 (< >) 括起来,放在类名的后面。
- 泛型类可以有多个类型变量,在括号中用逗号隔开。
- 类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。
- 用具体的类型替换类型变量就可以实例化泛型类型,换句话说,泛型类可看作普通类的工厂。
类型变量使用大写形式,且比较短,这是很常见的。
8.3 泛型方法
泛型方法
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");
- 注意,类型变量放在修饰符 (这里是 public static) 的后面,返回类型的前面。
在 C++ 中将类型参数放在方法名后面,有可能会导致语法分析的歧义。例如,b>©) 可以理解为“用 f<a, b>© 的结果调用 g”,或者理解为“ 用两个布尔值 f<a 和 b>c 调用 g
- 当调用一个泛型方法时在方法名前的尖括号中放人具体的类型。
类型推断
double middle = ArrayAlg.getMiddle(B.14, 1729, 0);
//error:解释这句代码有两种方法,而且这两种方法都是合法的。
//找到 2 个这样的超类型:Number 和 Comparable 接口。
//可以采取的补救措施是将所有的参数写为 double 值。
- 窍门 : 有目的地引入一个错误,并研究所产生的错误消息。
8.4 类型变量的限定
问题
- 类型为 T, 这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?
解决办法
- 可以通过对类型变量 T 设置限定(bound) 实现这一点:
public static <T extends Coiparab1e> T a) . . .
为什么使用extends关键字
- 表示 T 应该是绑定类型的子类型 (subtype)。T 和绑定类型可以是类,也可以是接口。
- 选择关键字 extends 的原因是更接近子类的概念,并且 Java 的设计者也不打算在语言中再添加一个新的关键字 (如 sub)。
使用
T extends Comparable & Serializable
- 一个类型变量或通配符可以有多个限定。
- 限定类型用“ &” 分隔,而逗号用来分隔类型变量。
- 如果用一个类作为限定,它必须是限定列表中的第一个。
8.5 泛型代码和虚拟机
8.5.1 类型擦除
原始类型
- 原始类型 (raw type)的名字就是删去类型参数后的泛型类型名。
- 擦除 (erased) 类型变量 , 并替换为限定类型 (无限定的变量用 Object)。
替换
- 原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。
读者可能想要知道切换限定: class Interval<T extends Serializable & Comparable>会发生什么。
- 如果这样做,原始类型用 Serializable 替换 T,而编译器在必要时要向 Comparable 插入强制类型转换。
- 为了提高效率,应该将标签(tagging) 接口 (即没有方法的接口) 放在边界列表的末尾。
8.5.2 翻译泛型表达式
强制类型转换
- 当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
//翻译为两条虚拟机指令:
//- 对原始方法 Pair.getFirst 的调用。
//- 将返回的 Object 类型强制转换为 Employee 类型。
- 当存取一个泛型域时也要插人强制类型转换。
//假设 Pair 类的 first 域和 second 域都是公有的,表达式也会在结果字节码中插人强制类型转换。
Employee buddy = buddies.first;
注意强制类型转换
- 一般来说父类是无法强制转换成子类的,除非这个父类是又子类先转换过来的。在泛型中,类型是确定的(传入的是实际类型),所以插入强制类型转换无问题。
8.5.3 翻译泛型方法
两个复杂问题
class Datelnterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirstO) >= 0)
super.setSecond(second);
}
}
//擦除后
class Datelnterval extends Pair { // after erasure
public void setSecond(LocalDate second) { . . . }
}
//存在另一个从 Pair 继承的 setSecond方法
public void setSecond(Object second)
//希望使用多态性
Datelnterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
- 问题在于类型擦除与多态发生了冲突。需要编译器在 Datelnterval 类中生成一个桥方法(bridge method):
//当作为 Pair<Loca1Date> pair 去调用时,调用实际的setSecond方法
public void setSecond(Object second) { setSecond((Date) second); }
- 编译器可能产生两个仅返回类型不同的方法字节码 (不能主动编写),虚拟机能够正确地处理这一情况。
//假设 Datelnterval 方法也覆盖了 getSecond 方法
public LocalDate getSecond() { return (Date) super,getSecond().clone(); }
//在 Datelnterval 类中,有两个 getSecond 方法
LocalDate getSecond() // defined in Datelnterval
Object getSecond0 // overrides the method defined in Pair to call the first method
Java 泛型转换的事实
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插人强制类型转换。
8.5.4 调用遗留代码
传递给原始类型时
void setLabelTable(Dictionary table)
//填充字典时, 要使用泛型类型
Dictionary<Integer, Component> labelTable = new Hashtableo();
labelTable.put(0, new JLabel(new Imagelcon("nine.gif")));
labelTable.put(20, new JLabel(new ImageIcon("ten.gif")));
- 编译器会发出一个警告,未来的操作有可能会产生强制类型转换的异常。
- 这个警告对操作不会产生什么影响,最多考虑一下原始类型有可能用泛型对象做什么就可以了 (自己确认无误,即可忽略)。
原始类型赋值给参数化类型变量
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // Warning
- 这样做会看到一个警告,人为确保不会有问题即可忽略。
- 这种情况并不会比有泛型之前的情况更糟糕。最差的情况就是程序抛出一个异常。
忽略警告
//使用注解,须放在生成这个警告的代码所在的方法之前
@SuppressWarnings("unchecked")
Dictionary<Integer, Components〉labelTable = slider.getLabelTableQ; // No warning
//或者,可以标注整个方法
@SuppressWarnings("unchecked")
public void configureSliderO { . . . }
- 可以利用注解 ( annotation) 使之消失。
8.6 约束与局限性
8.6.1 不能用基本类型实例化类型参数
原因
- 擦除之后,可以生成含有 Object 类型的域,而 Object 不能存储 double值。
- 当包装器类型(wrapper type) 不能接受替换时,可以使用独立的类和方法处理它们。
8.6.2 运行时类型查询只适用于原始类型
解释
- 虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
//a instanceof Pair ???
Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair
Pair<String> stringPair = . . .;
Pair<Employee> employeePair = . . .;
if (stringPair.getClass() == employeePair.getClass()) // they are equal
//其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class。
- 倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
- 同样的道理,getClass 方法总是返回原始类型。
8.6.3 不能创建参数化类型的数组
原因
Pair<String>[] table = new Pair<String>[10]; // Error
Object[] objarray = table;//擦除后
objarray[0] = "Hello"; // Error component type is Pair
//不过对于泛型类型,擦除会使这种机制无效。
objarray[0] = new Pair<Employee>();//可以通过检查
- 对于泛型类型,虽然能够通过数组存储存检查,但是可能会导致类型错误,所以不允许创建参数化类型的数组。
- 需要说明的是,只是不允许创建参数化类型的数组,而声明变量仍是合法的,不过不能通过 new 初始化该变量。
- 可以声明通配类型的数组,然后进行类型转换 (所有对象都可以赋值给 “?” ,不会发生类型转换错误)。
//结果将是不安全的
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10]{{add(Employee ...)}};
8.6.4 Varargs 警告
问题
- 上一节中已经了解到,Java 不支持泛型类型的数组。
- 问题:向参数个数可变的方法传递一个泛型类型的实例?
解决
public static <T> void addAll(Collections coll, T... ts) {
for (t : ts) coll.add(t);
}
addAll(table, pairl, pair2);//调用
- Java 虚拟机必须建立一个 Pair<String> 数组,这就违反了前面的规则。
- 不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。
取消警告两种方法
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
- 一种方法是为包含 addAll 调用的方法增加注解 @SuppressWamings(“unchecked”) 。
- 在 Java SE 7中,还可以用 @SafeVarargs 直接标注addAll 方法。
消除创建泛型数组的有关限制
- 如前文,声明通配类型的数组,然后进行类型转换。
//结果将是不安全的
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10]{{add(Employee ...)}};
- 使用参数个数可变的方法
@SafeVarargs static <E> E[] array(E... array) { return array; }
//调用
Pair<String>[] table = array(pairl,pair2);
//隐藏着危险
Object[] objarray = table;
objarray[0] = new Pair<Employee>();
//在处理 table[0] 时你会在别处得到一个异常。
- 使用反射
class GenericsArray {
@SuppressWarnings({ "unchecked", "hiding" })
public static <T> T[] getArray(Class<T> componentType,int length) {
return (T[]) Array.newInstance(componentType, length);
}
}
8.6.5 不能实例化类型变置
举例
public Pair() { first = new T(); second = new T(); } // Error
- 不能使用像 new T(…) ,newT[…] 或 T.class 这样的表达式中的类型变量。
- 类型擦除将 T 改变成 Object, 而本意肯定不希望调用 new Object()。
解决办法
//JAVA 8 匿名表达式
Pair<String> p = Pair.makePairCString::new);
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
//反射
first = T.class.newInstance(); // Error
public static <T> Pair<T> makePair(Cl ass<T> cl) {
try {
return new Pair<>(d.newInstance(), cl.newInstance());
}catch (Exception ex) {
return null;
}
}
Pair<String> p = Pair.makePair(String.class);
- 在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。
- 比较传统的解决方法是通过反射调用 Clasmewlnstance 方法来构造泛型对象。
Class类
- 注意,Class类本身是泛型。
- 例如,String.class 是一个 Class<String> 的实例 (事实上,它是唯一的实例) 。因此,makePair 方法能够推断出 pair 的类型。
8.6.6 不能构造泛型数组
原因
public static <T extends Comparable> T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // Error
//类型擦除会让这个方法永远构造 Comparable[2] 数组。
- 虽然数组会填充 null 值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。
类型转换问题
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
}
- 如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为 Object[],并且在获取元素时进行类型转换。
//实际的实现没有这么清晰
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
}
//编译时不会有任何警告。当 Object[] 引用赋给 Comparable[] 变量时,将会发生 ClassCastException异常。
String[] ss = ArrayAlg.minmax("Tom", "Dick", "Harry");
解决办法
- 在这种情况下,最好让用户提供一个数组构造器表达式(Java 8):
String[] ss = ArrayAlg.minmax (String[]::new,"Tom", "Dick", "Harry");
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] mm = constr.apply(2);
//虽然类型T擦除为了Comparable,但是new出来的是实际类型,赋值给了Comparable[],用到了多态,不会出现类型转换问题。
}
- 比较老式的方法是利用反射,调用 Array.newlnstance:
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = (T[]) Array.newlnstance (a.getClass().getComponentType() , 2);
//为什么 a 可以获得类型呢?因为 a 是传入的变量,类型确定,此处 a 是string数组类型,
}
toArray方法问题
- ArrayList 类的 toArray 方法需要生成一个 T[] 数组,但没有成分类型。(不能传出构造表达式,不能获得成分类型,上述两种方法失效了)
- 有下面两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
//第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用result的成分类型构造一个足够大的新数组。
一点小思考
- 为什么不能在 toArray() 方法中读取参数类型,创建指定的数组呢?
- 在ArrayList中存的是擦除后的Obeject数组,而且在创建泛型数组的时候并没有提供构造器或者用于反射的类型,实际创建的是Obeject数组,所以不能得到实际的类型,生成泛型数组返回也就不可能了。
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<Random> 共享随机数生成器,声明一个 Singleton<JFileCh00Ser> 共享文件选择器对话框。
- 不能在静态域或方法中引用类型变量。
- 类型擦除之后,只剩下 Singleton 类,它只包含一个 singlelnstance 域,实际是希望对于不同的 T 有不同的作用。
并未生成对于多个 T 的不同 Singleton 类,只有一个包含 Object 类型的 Singleton 类。
8.6.8 不能抛出或捕获泛型类的实例
具体内容
public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable
//以下方法将不能编译
public static <T extends Throwable〉void doWork(Class<T> t) {
try{
do work
}catch (T e) { // Error can 't catch type variable
Logger.global.info(...);
}
}
- 记住一点,异常一定是确定的!
- 既不能抛出也不能捕获泛型类对象。
- 实际上,甚至泛型类扩展 Throwable 都是不合法的。
合法使用
public static <T extends Throwable〉void doWork(T t) throws T { // OK
try{
do work
}catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
//实际是对捕获的异常进行包装,并未去捕获泛型异常
- 不过,在异常规范中使用类型变量是允许的。
8.6.9 可以消除对受查异常的检查
消除异常限制
@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
//编译器就会认为 t 是一个非受查异常。
Block.<RuntimeException>throwAs(t);
- Java 异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。
使用意义
- 在声明为不抛出任何受查异常的方法中,不去捕获方法中的受查异常、包装到非受查异常,只是抛出异常,并“哄骗” 编译器,让它认为这不是一个受查异常。
一点想法
- 正常情况下是用一个新的非受查异常包装抛出的异常。
RuntimeException t = ...;
try{
do work
}catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
- 错误想法:
而这里是却是利用了泛型不检查类型转换的特点 (在取值的时候由编译器强转,标注的泛型强转其实未生效),将Throwable 强制转换成了 RuntimeException 并抛出,哄骗编译器,实际抛出的异常就是原来的受查异常!
static class A {}
static class B extends A {}
static class C extends A {}
//试试是否真的不检查类型转换
static <T extends A> T transfer(A e){
return (T) e;
}
//实际操作
B bb = new B();
C cc = Main.<C>transfer(bb);//ClassCastException: Main$B cannot be cast to Main$C
//此处报错,但错误轨迹未到tranfer函数中去,说明了确实是在取值的时候强制类型转换,但是编译器实际没有忽略类型转换错误的检查。
//那么问题来了,如果不去取值呢?
Main.<C>transfer(bb);//编译能通过,无问题
//结论也就是说,不去取值不会触发编译器强制转换,这点很重要!
- 实际操作后:
- 为什么没有发生 FileNotFoundException 到 RuntimeException 的强制类型转换错误呢?
- 结合上面例子,因为没拿着 RuntimeException 的变量去要抛出的异常啊!所以没有强制类型转换。
- 为什么没强制类型转换还能抛出受查异常呢?
- 因为 throwAs(e) 方法声明了抛出泛型异常,而此时抛出的泛型的异常是RuntimeException,不是受查异常。
- 为什么可以抛出泛型异常呢?不是不可以抛出泛型异常吗?
- 对于调用 throwAs(e) 方法,每次异常都是确定,而且并不是抛出泛型异常,只是假装强制转换了一下 (甚至都没生效)。
private static void testException() {
Scanner in = null;
try {
in = new Scanner(new File("ququx"), "UTF-8");
while (in.hasNext())
System.out.println(in.next());
} catch (Throwable e) {
Main.<RuntimeException>throwAs(e);
}
}
//实际输出FileNotFoundException
try {
testException();
}catch (RuntimeException e) {
//实际不生效,catch的还是实际异常
System.out.println("RuntimeException!")
}catch (Exception e) {
System.out.println("FileNotFoundException!");
}
8.6.10 注意擦除后的冲突
问题
public class Pair<T> {
public boolean equals(T value) {
return first,equals(value) && second,equals(value);
}
}
//对于Pair<String>,类型擦除后
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object
//实际上有两个boolean equals(Object)
boolean equals(T) 擦除成了 boolean equals(Object)
- 当泛型类型被擦除时,无法创建引发冲突的条件。
一个原则
- 要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。
存在问题
class Employee implements Comparable<Emp1oyee> {...}
class Manager extends Employee implements Comparable<Manager> { . . . } //error
//Manager 会实现 Comparable<Employee> 和 Comparable<Manager>, 这是同一接口的不同参数化。
- 其原因非常微妙,有可能与合成的桥方法产生冲突。
//实现了 Compamble<X> 的类可以获得一个桥方法:
public int compareTo(Object other) { return compareTo((X) other); }
//对于不同类型的 X 不能有两个这样的方法。
8.7 泛型类型的继承规则
举例
Manager[] topHonchos = . .
Pair<Employee> result = ArrayAlg.minmax(topHonchos); // Error
//minmax 方法返回 Pair<Manager>,而不是 Pair<Employee>,并且这样的赋值是不合法的。
- 考虑一个类和一个子类,如 Employee 和 Manager。Pair<Manager> 是Pair<Employee> 的一个子类吗? 答案是“ 不是”。
存在问题
- 数组带有特别的保护,会记住原始的类型,设置错误的类型会抛出 ArrayStoreException 异常。但是参数化类型不会。
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies; // illegal , but suppose it wasn't
//下面语句没问题,但是不应该
employeeBuddies.setFirst(lowlyEmployee);
- 永远可以将参数化类型转换为一个原始类型。可能发生类型转换错误。
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File(". . .")); // only a compile-time warning
//但是,请记住现在的状况不会再比旧版 Java 的情况糟糕。这里失去的只是泛型程序设计提供的附加安全性。
泛型列表间的联系
- 泛型类可以扩展或实现其他的泛型类。
- 一个 ArrayList<Manager> 可以被转换为一个 Lis<Manager>,但是一个 ArrayList<Manager> 不是一个ArrayList <Employee> 或 List<Employee>。
8.8 通配符类型
8.8.1 通配符概念
概念
- 通配符类型中,允许类型参数变化。
- 写法:Pair<? extends Employee>
解决泛型关系问题
- 使用通配符类型。
正如前面讲到的,不能将 Pair<Manager> 传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型:
public static void printBuddies(Pair<? extends Eiployee> p)
提出可能存在问题
- 使用通配符会通过 Pair<? extends Employee> 的引用破坏 Pair<Manager> 吗?
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wi1dcardBuddies.setFirst(lowlyEmployee); // compile-time error
- 对 setFirst 的调用有一个类型错误,使用 getFirst 就不存在这个问题。
//Pair<? extends Employee> 其方法似乎是这样的
? extends Employee getFirst()
//这样将不可能调用setFirst方法。编译器只知道需要某个Employee的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来配。
void setFirst(? extends Employee)
//使用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee的引用完全合法。
8.8.2 通配符的超类型限定
超类型限定
- 通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定 (supertypebound):? super Manager。
为什么要这样做呢?
- 带有超类型限定的通配符的行为与 8.8.1 节介绍的相反。可以为方法提供参数,但不能使用返回值。
void setFirst(? super Manager)
//编译器无法知道setFirst方法的具体类型,因此调用这个方法时不能接受类型为Employee或Object的参数。只能传递Manager类型的对象,或者某个子类型(如 Executive)对象。
? super Manager getFirst()
//如果调用getFirst,不能保证返回对象的类型。只能把它赋给一个Object。
带有超类型限定的通配符关系
使用小结
- 直观地讲,带有超类型限定的通配符可以向泛型对象写人,带有子类型限定的通配符可以从泛型对象读取。
另一种应用
//Comparable 接口本身就是一个泛型类型
public interface Comparable<T> {
public int compareTo(T other);
}
//对于String类实现Comparable<String>
public int compareTo(String other)
//接口是一个泛型接口之前,other是一个 Object
public int compareTo(Object other)
//考虑使用子类型限定的min方法,比只使用T extents Comparable更彻底
public static <T extends Comparable<T> T min(T[] a)
//例如,如果计算一个String数组的最小值,T就是String类型的,而String是Comparable<String>的子类型。
//出现问题!
//但是,处理一个LocalDate对象的数组时,会出现一个问题。LocalDate实现了ChronoLocalDate, 而ChronoLocalDate扩展了 Comparable<ChronoLocalDate>。因此,LocalDate实现的是 Comparable<ChronoLocalDate> 而不是Comparable<LocalDate>。
//在这种情况下,超类型可以用来进行救助:
public static <T extends Comparable<? super T>> T min(T[] a)...
//现在 compareTo 方法写成
int compareTo(? super T)
//限定的是T的超类,无论如何,传递一个T类型的对象给compareTo方法都是安全的。
作为函数式接口的参数类型
//例如Collection接口有一个方法:
default boolean removelf(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<?>。
? getFirst()
//getFirst的返回值只能赋给一个 Object。
void setFirst(?)
//setFirst方法不能被调用,甚至不能用 Object调用。(类型可变,不确定,但是可以调用setFirst(null))。
- Pair<?> 和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法。
为什么这样使用
- 它对于许多简单的操作非常有用。
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<?>类型的参数。
public static void swap(Pair<?> p) { swapHelper(p); }
- 在这种情况下,swapHelper 方法的参数 T 捕获通配符。
- 它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且 <T>swapHelper 的定义只有在 T 指出类型时才有明确的含义。
例子
//在这里,通配符捕获机制是不可避免的。
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
PairAlg.swap(result); // OK swapHel per captures wildcard type
}
捕捉的条件
- 编译器必须能够确信通配符表达的是单个、确定的类型。
例如, ArrayList<Pair<T>> 中的 T 永远不能捕获 ArrayList<Pair<?>>中的通配符。
ArrayList<Pair<?>>中可以保存多个对于不同?的类型的 Pair<?>。
8.9 反射和泛型
8.9.1 泛型 Class 类
class类
- 现在,Class 类是泛型的。
- 例如,String.class 实际上是一个Class<String> 类的对象 (事实上,是唯一的对象)。
class类中泛型方法
T newInstance()
//newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得 (生成了实际的类型)。
//它的返回类型目前被声明为 T,其类型与 Class<T> 描述的类相同,这样就免除了类型转换。
T cast(Object obj)
//如果给定的类型确实是 T 的一个子类型,cast方法就会返回一个现在声明为类型 T 的对象,否则,抛出一个BadCastException异常。
T[] getEnumConstants()
//如果这个类不是 enum 类或类型 T 的枚举值的数组,getEnumConstants 方法将返回 null。
Class<? super T> getSuperclass()
Constructors getConstructor(C1ass... parameterTypes)
Constructors getDeclaredConstructor(Class... parameterTypes)
//最后,getConstructor 与 getdeclaredConstructor 方法返回一个 Constructor<T> 对象。
//Constructor 类也已经变成泛型,以便 newlnstance 方法有一个正确的返回类型。
- 主要注意构造方法和 newInstance()方法。
8.9.2 使用 Class 参数进行类型匹配
Class<T>类型匹配
//有时,匹配泛型方法中的 Class<I> 参数的类型变量很有实用价值。
public static <T> Pai r<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 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。
public static Comparable min(Coniparable[] a)
//这是一个泛型方法的擦除
public static <T extends Comparable<? super T>> T min(T[] a)
例如, 原始的 Pair 类知道源于泛型类 Pair<T>, 即使一个 Pair 类型的对象无法区分是由 Pair<String> 构造的还是由 Pair<Employee> 构造的。
可以使用反射 API 来确定:
- 这个泛型方法有一个叫做 T 的类型参数。
- 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
- 这个限定类型有一个通配符参数。
- 这个通配符参数有一个超类型限定。
- 这个泛型方法有一个泛型数组参数。
接口 Type 子类型
- Class 类,描述具体类型。
- TypeVariable 接口,描述类型变量 (如 T extends Comparable<? super T>) 。
- WildcardType 接口,描述通配符 (如?super T)。
- ParameterizedType 接口,描述泛型类或接口类型 (如Comparable<? super T>)。
- GenericArrayType 接口,描述泛型数组 (如 T[ ])。
Type 继承层次
一点思考
- 可以使用反射通过对class文件进行操作,获得泛型信息,那擦除的类保留的泛型祖先的微弱记忆在哪?
- 擦除后的class文件中?唯一的Class<T>中?
泛型反射 API 打印
- 书本P340,代码较多,但是很详细,看懂能有深入理解。