1. 类型变量的限定
有时候,泛型类、接口或者方法会需要对类型变量加以约束。例如,规定此类型变量指向的对象必须是实现了 Comparable 接口的。这时候,可以通过对类型变量 T 设置限定(bound)来实现这一点。
public <T extends Comparable>void sort(T t) { ... }
在上述例子中,调用此方法时,传入的对象必须实现了 Comparable 接口,否则就会产生一个编译错误。这里大家可能会有一个疑惑,为什么使用关键字 extends?Comparable 是一个接口,那不是应该用 implements 吗?
注意,此处的 extends 其实并不表示继承,可以将其理解成“绑定”,“绑定”的可以是类,也可以是接口,表示 T 应该是绑定类型的子类型。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字。
当然,一个变量类型或通配符可以有多个限定,但其中只能有一个类。这也符合我们原本的知识体系——只能继承一个类,但能实现多个接口。
public class Practical<T extends Person & ActionListener & MouseListener> { ... }
如果用一个类作为限定,则它必须是限定列表中的第一个,不同的限定类型之间用“ &” 分隔。
2. 通配符类型
通配符是Java泛型中的一个重要的概念,对于泛型类之间的继承关系等具有重大的意义。在了解什么是通配符之前,先来了解一下泛型类型的继承规则。
2.1 泛型类型的继承规则
首先思考一个问题,现在有一个 Employee 类和一个继承了 Employee 类的 Manager 类,如下所示:
public class Employee { ... }
public class Manager extends Employee { ... }
那么对于一个普通的泛型类 Practical:
public class Practical<T> { ... }
类型 Practical< Manager > 是 Practical< Employee > 的子类吗?答案是否定的,通过下面这段代码可以对此进行验证:
Practical<Manager> manager = new Practical<>();
Practical<Employee> employee = manager; // error
上述代码的第二行,用一个 Practical< Employee > 类型的变量引用了一个 Practical< Manager > 类型的对象,然而编译器显示这样是不合法的。我们知道,如果 B 是 A 的子类的话,是可以用 A 类型的变量引用 B 类型的对象(向上造型)的。显然,这个例子证明了 Practical< Manager > 并不是 Practical< Employee > 的子类。
也许这看起来会很奇怪,但这样设定是出于安全性的考虑,可以避免一些风险的出现。
public class Practical<T> {
ArrayList<T> list;
public Practical() {
list = new ArrayList<>();
}
public void add(T t) {
list.add(t);
}
public T getFirst() {
// return the top ranked;
}
}
现在我们补全了一部分 Practical 类的内容,然后执行以下操作:
Practical<Manager> manager = new Practical<>();
Practical<Employee> employee = manager; // 假设可以
manager.add(new Manager());
employee.add(new Employee());
Employee firstEmployee = manager.getFirst();
观察上面的代码,就会发现很多不合理之处,首先第一行实例化了一个新的对象之后,此对象中的 list 列表的成员按道理应该是 Manager 类型的,在第三行向 list 中加入了一个 Manager 类型的对象之后,第四行又加入了一个 Employee 类型的对象(???此处 Employee 可是 Manager 的父类)。在此之后还选出了 list 中的第一名(暂且理解为绩效最好的吧),将“经理”和“普通员工”放一起比较显然是不合理的。
当然,Java泛型类型的继承规则保证了这种“离谱”的情况将不会发生!
2.2 通配符类型
正如上面所提到的,Practical< Manager > 并不是 Practical< Employee > 的子类,这样做虽然是安全的,但使用起来并没有那么令人愉快。为此,Java 的设计者发明了一种巧妙的(仍然是安全的)“ 解决方案”:通配符类型。
通配符类型中, 允许类型参数变化。例如, 通配符类型 Practical<? extends Employee>
表示任何符合规定的泛型 Practical 类型,即它的类型参数是 Employee 的子类,如 Practical< Manager >,但不是 Practical< String >。
假设要编写一个打印雇员对的方法:
public void printEmployee(Practical<Employee> p) { ... }
正如前面讲到的,不能将Practical< Manager > 传递给这个方法,这一点很受限制。解决的方法很简单——使用通配符类型:
public void printEmployee(Practical<? extends Employee> p) { ... }
类型 Practical< Manager > 是 Practical<? extends Employee> 的子类型,这样做就可以很好的满足此场景的需求。不过,此时又会有新的疑问了,这样会打破之前所说的Java泛型类型的继承规则带来的安全性吗?答案当然是否定的。对于 Practical<? extends Employee>,其 add 方法和 getFirst 方法似乎是这样的:
// 注意编程时不能这样定义具体的方法,通配符并不是类型变量。
// 因此,不能在编写代码中使用“ ?” 作为一种类型,这里只是为了方便表述。
public void add(? extends Employee t) { ... }
public ? extends Employee getFirst() { ... }
对于 add 方法,编译器只知道它需要某个 Employee 的子类型,但并不知道具体是哪种类型。因此,它拒绝传递任何特定的类型。简单的来说,使用 extends 关键字的通配符类型时,带有类型变量参数的方法将无法被调用。
Practical<? extends Employee> p = new Practical<>();
p.add(new Employee()); // error
/* The method add(capture#1-of ? extends Employee)
* in the type Practical<capture#1-of ? extends Employee>
* is not applicable for the arguments (Employee)
*/
当其作为返回值时,是被允许的,即上面的 getFirst 方法依旧可以被调用(接收的变量需要是 Employee 类型或者其父类)。
Employee e = p.getFirst(); // ok
这也不难理解,对于传入的参数,由于其具体类型不能完全确定,因此盲目的传入某类型可能会造成某些不希望得到的结果(比如 2.1 中的例子)。但如果其作为返回值,因为我们已经知道了返回类型必定是 Employee 的子类,那么就可以用 Employee 类型的变量去引用它(Employee 所拥有的数据域和方法,其子类必定也有)。
2.3 通配符的超类限定(super)
除了 extends 关键字之外,通配符类型还可以可以指定一个超类型限定,如下所示:
Practical<? super Employee> p = new Practical<>();
注意,只有通配符类型可以使用 super 关键字进行限定,前面第一节中提到的类型变量的限定并不行。
与 extends 关键字相反,super 关键字表示 Employee 的所有父类型。同样的,其支持的“行为”也正好与 extends 关键字相反,可以作为方法的参数类型(可以传入限定类型及其子类型,即下面例子中Employee及其子类),但不能作为返回值类型。 对应于上一节中的解释,相信这并不难理解。
Practical<? super Employee> p = new Practical<>();
p.add(new Employee()); // ok
Employee e = p.getFirst(); // error
2.4 无限定通配符
还可以使用无限定的通配符, 例如,Practical<?>。乍一看这好像与原始的 Practical 类型一样,实际上, 它们有很大的不同:
public void add(? t) { ... }
public ? getFirst() { ... }
getFirst 方法的返回值只能赋给一个 Object 类型的变量,而 add 方法不能被调用, 甚至不能通过传入一个 Object 类型的对象调用它。
3. 总结
- 类型变量(如 T)可以用 extends 关键字进行绑定,表示 T 应该是绑定类型的子类型,但无法使用 super 关键字。
- 对于通配符 ?,可以用 extends 和 super 关键字进行限定,也可以使用无限定的通配符。
- 对于使用 extends 关键字限定的和无限定的通配符,可以作为返回值,但不能作为传入的参数类型。
- 对于使用 super 关键字限定的的通配符则正好相反,不能作为返回值,但可以作为传入的参数类型。