【CoreJava】泛型程序设计
8 泛型程序设计
8.1 泛型的发生
在泛型之前,大家为了复用代码,使用的是继承机制,以 ArrayList 举例,最开始:
public class ArrayList {
private Object[] elementData;
public Object get(int i);
...
}
这种做法有两个问题:
- 获取的值必须强转
- 没有类型检查,任何类型的对象都可以往里面塞
为了解决这两个问题,引入了类型参数,产生了泛型:
ArrayList<String> files = new ArrayList<>();
8.2 定义泛型类
以下泛型类引入了类型变量 T,并用尖括号包围起来,一个泛型类可以有多个类型变量。
一般的,类型变量都为大写形式,E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型,T、U 和 S 表示任何类型。
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 first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
8.3 泛型方法
泛型方法可以定义在普通类中,也可以定义在泛型类中。
定义泛型方法就像定义泛型类一样,将类型参数放在修饰符的后面即可。
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
调用时只需要指名具体的类型,绝大多数情况下不需要指名,编译器能够推断出类型:
String middle = ArrayAlg.<String>getMiddle(words);
如果所给定的参数不是统一的,那么编译器将会自动找寻它们的公共超类(类或者接口)。
8.4 限定类型变量
泛型的出现可以实现代码的复用,但是,有些代码往往仅限于一部分类型的元素使用,比如:
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;
}
上述方法仅仅适用于实现了 Comparable 的元素类型 T,但 Client 往往不会注意这些(除非注解说明)。为了解决这个问题,引入类型限定,将方法签名更改成:
pulic static <T extends Comparable & Serializable> T min(T[] a)
其中,&
符号用来表示多重限定的并列,逗号则用来分隔类型变量。
8.5 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类!
8.5.1 类型擦除
如果泛型类的泛型参数(泛型变量)存在限定(extends),那么类将会被擦除还原成对应的限定,如果没有,则被擦除为 Object。
当调用泛型方法时,编译器将会自动插入类型转换,比如
String first = myPair.getFIrst();
将会被这样执行(大致原理):
Object object = myPair.getFIrst();
String first = (String) object;
桥方法,就是一个类继承自一个泛型类方法,因为类型擦除的原因,可能会与多态发生冲突。
8.6 约束和局限性
不能用基本类型实例化类型参数,不能创建 Pair<int>
的对象,因为类型擦除后的 Object 不能引用 int 基本类型变量。
使用 instanceof 判断一个对象是否是泛型类型将会抛出编译器错误,而对泛型使用强制类型转换则会得到警告:
if (a instanceof Pair<String>)
if (a instanceof Pair<T>)
Pair<String> p = (Pair<String>) a // warning
stringPair.getClass() == employeeClass.getClass(); // equal
无法初始化数组泛型类型数组,但是可以声明变量,这是因为数组需要一个明确的数据类型,但是类型擦除后的类型是 Object,可以存储任意类型的对象,导致数组抛出 ArrayStoreException 的异常。所以我们往往使用 List 来收集参数化类型对象:
Pair<String>[] stringPairs; // OK
new Pair<String>[10]; // NO!
@SafeVarargs 注解来压制多参方法的问题。
public static <T> void addAll(Collections coll, T... ts) {
for (t : ts) coll.add(t);
}
// 调用
Col1ection<Pair<String» table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);
这时候,系统就不得不为我们创建 Pair<String> 类型的数组,前面又说道 Java 不允许创建参数化类型对象数组,虽然这种调用不会导致系统抛出异常,但是会收到警告,为了压制警告,可以使用两种注解:
@SuppressWamings("unchecked")
@SafeVarargs
不能实例化类型变量。
下面的构造器就是错误的,不能使用类型变量 T 来作为表达式的成分,因为类型擦除后,T 就变成了 Object,不符合我们原本的愿望。
public Pair() { first = new T(); second = new T(); } // Error
而最好的解决办法就是提供一个构造器表达式:
Pair<String> p = Pair.makePair(String::new);
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get().constr.get());
}
makePair
方法接收一个Supplier<T>
, 这是一个函数式接口, 表示一个无参数而且返回类型为 T 的函数。为了得到类型变量的对象,可以使用反射的方法来实现,但这种奇技淫巧还是不用为好:
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.newInstance, cl.newInstance);
} catch (Exception ex) {
return null;
}
}
Pair.makePair(String.class)
String.class 将会生成 Class<String> 的实例,进一步生成 Pair<String> 的实例。
8.7 泛型类型的继承规则
无论 S 和 T 有什么联系,Pair<S> 和 Pair<T> 是没有联系的!除非泛型类之间就已经存在了继承实现关系,比如 ArrayList<T> 实现了 List<T>,那么 ArrayList<String> 就可以被 List<String> 引用。
8.8 通配符类型
泛型虽好,倘若一定需要指定类型变量来使用泛型(没有 8.7 中提到的更为直接的继承关系),那就不是那么友好,因此引入了通配符类型。
8.8.1 通配符概念
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
Systefn.out.println(first.getName() + " and " + second.getNameQ + " are buddies.");
}
这个方法只能被 Pair<Employee> 调用,Pair<Manager> 无法享用。很不人性化,而下面的改进则解决了这个问题
public static void printBuddies(Pair<? extends Eiployee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
Systefn.out.println(first.getName() + " and " + second.getNameQ + " are buddies.");
}
其继承关系变成了这样,那么类型变量为 Manager 和 Employee 的泛型实例就可以是用这个方法了,因为它们的泛型类都继承自 Pair<? extends Employee>
8.8.2 通配符的超类型限定
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
8.8.3 无限定通配符
Pair<?> 和 Pair 原始类型似乎相同,但它们最大的区别就是:可以用任意Object 对象调用原始 Pair 类的 setObject 方法,但是 Pair<?> 不能。
使用 Pair<?> 的好处在于能够普适快速读取的应用,比如判断一个 Pair 中是否含有 null:
public static boolean hasNulls(Pair<?> p) {
return p.getFirstO = null || p.getSecondO =null;
}
// 上面的方法可以换成以下写法,但是明显上面的方法可读性更强,更加简便
public static <T> boolean hasNulls(Pair<T> p)
8.8.4 捕获通配符
通过辅助的泛型方法来实现通配符的捕获。
还有一点要注意的就是,使用 extends 和 super 修饰通配符的时候的 get 和 set 的问题。
8.9 反射和泛型
引入了泛型类之后,CLass 类也是泛型的,String.class 实际上是 Class<String> 的一个对象(唯一对象)。