前言
本章主要内容摘录自 Java 核心技术 卷1 基础知识,主要是泛型的一些基础知识。
正文
泛型类
泛型类(generic class)就是具有一个或多个类型变量的类。下面给出一个简单的泛型类例子:
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 void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair 类引入了一个类型变量 T,用尖括号(<>)括起来,并放在类名的后面。
泛型类可以有多个类型变量。例如,可以定义 Pair 类,其中第一个域和第二个域使用不同的类型:public class Pair<T, U> {...}
泛型类中定义的类型变量可以用来指定方法的返回类型以及域和局部变量的类型。
:::info 类型变量使用大写形式,且比较短。一般,使用变量 E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型。T(需要时还可以用临近的字母 U 和 S)表示“任意类型”。 :::
泛型类的实例化比较简单,使用具体的类型替换类型变量就可以实现。例如:Pair<String> pair = new Pair<>();
泛型方法
泛型方法就是定义一个带有类型参数的简单方法。下面给出一个简单的泛型方法例子:
public class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
注意:类型变量放在修饰符(这里是 public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放人具体的类型:String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
,但是,一般我们在调用泛型方法的时候不需要显示的指定类型参数,编译器可以根据上下文推断出所调用的方法使用的真实类型,比如:当我们传入 getMiddle 方法的是一批字符串数据,则编译器会使用实参的类型 String[] 与泛型类型 T[] 进行匹配并推断出 T 一定是 String。也就是说,可以这样调用:String middle = ArrayAlg.getMiddle("John", "Q.", "Public");
大多数情况下,编译器都能够推断出真实类型。但是,当我们传入到 getMiddle 方法中的参数存在类型不一致的情况时,编译器也会提示错误。看一看下面这个例子:
double middle = ArrayAlg.getMiddle(3.14, 1729, 0);
编译器将会自动打包参数为 1 个 Double 和 2 个 Integer 对象,而后寻找这些类的共同超类型。事实上,找到 2 个这样的超类型:Number 和 Comparable 接口,即表示可以将结果赋给两种类型。所以,这里建议将所有的参数都写成 double 值。
类型变量的限定
有时,类或方法需要对类型变量加以约束。比如下面这个例子,我们要计算数组中的最小元素:
public class ArrayAlg {
public static <T extends Comparable> 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;
}
}
但是,编译器提示了一个问题,变量 smallest 类型为 T,这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?
解决这个问题的方案是将 T 限制为实现了 Comparable 接口的类:public static <T **extends Comparable**> T min(T[] a) ...
。在讲到通配符类型的时候会进一步扩展该方法,使其更加通用。
另外,一个类型变量可以有多个限定,例如:T extends Comparable & Serializable, U extends Comparable
,限定类型用“&”分隔,而逗号用来分隔类型变量。
在 Java 的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
泛型类型的继承规则
这里只介绍部分规则,还有一部分规则在介绍通配符类型的时候说明。泛型类型的继承规则用一幅图基本就可以描述清楚:
如上图所示,泛型类可以扩展或实现其他的泛型类。例如,ArrayList
另外,永远可以将参数化类型转换为一个原始类型。例如,可以这样写:
ArrayList<Manager> managers = new ArrayList<>();
List raws = managers;
raws.add("Manager");
如上所示,转换成原始类型之后,会失去泛型程序设计提供的附加安全性。raws 在添加新的元素的时候不会再检查元素的类型,当 raws 添加一个 “Manager” 字符串的时候,会抛出 ClassCastException 异常。
通配符类型
我们先来看泛型的一些概念:
- ArrayList
中的 E 称为类型参数变量。 - ArrayList
中的 Integer 称为实际类型参数。 - 整个 ArrayList
称为泛型类型。 - 整个 ArrayList
称为参数化的类型(ParameterizedType)。
通配符的作用:
- 通配符是用来解决泛型无法协变的问题的。
- 协变指的是如果 Child 是 Parent 的子类,那么 List
也应该是 List 的子类。但是,根据泛型类型的继承规则,我们知道泛型是不支持协变的。
- 协变指的是如果 Child 是 Parent 的子类,那么 List
- 泛型 T 是一个确定的类型,而通配符则更为灵活或者说是不确定。
- 通配符不是类型参数变量,或者说通配符和类型参数变量 T 不是一类东西。
- 你可以理解成泛型 T 就像是一个变量,等着你将来传一个具体的类型使用的,而通配符则是一种规定,规定你能传哪些参数。
通配符的使用方式:
- 通配符后面只允许有一个限定,例如
Pair<? extends Employee>
orPair<? super Manager>
。 - 通配符主要用于的变量声明以及形参列表,不能用来定义泛型类、泛型接口、泛型方法。
- 在实例化泛型类的时候也不可以使用通配符,例如
new Pair<?>()
。通配符的子类型限定
通配符有两种类型的限定,一种是子类型限定:? extends Employee
,另一种是超类型限定:? super Manager
。接下来分别介绍这两种类型的使用场景。
指定一个子类型限定:? extends Employee
,这个通配符限制为 Employee 的所有子类型。
假设我们要编写一个打印雇员对信息的方法:
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
正如前面讲到的,不能将 Pairpublic static void printBuddies(Pair<? extends Employee> p)
。
这里补充泛型类型的继承规则,类型Pair<Manager>
是Pair<? extends Employee>
的子类型:
使用通配符的子类型限定虽然解决了泛型协变的问题,但是这样也对泛型类的写操作产生限制,即只可以取值,不可以设值。
例如,如下代码,编译器会提示错误。
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
当我们使用类型Pair<? extends Employee>
的时候,其方法似乎是这样的:
? extends Employee getFirst()
void setFirst(? extends Employee e)
编译器只知道 setFirst 方法需要一个 Employee 的子类型,具体什么类型不知道,所以它拒绝传递任何特定的类型。而使用 getFirst 方法不存在这个问题,因为 getFirst 方法的返回值肯定可以赋给一个 Employee 的引用。
通配符的超类型限定
接着,我们介绍另外一种类型,指定一个超类型限定(super type bound):? super Managere
,这个通配符限制为 Manager 的所有超类型。
案例:有一个经理的数组,想把奖金最高和最低的经理放在一个 Pair 对象中。在这里,Pair