Java泛型(四):类型变量的限定、通配符类型

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. 总结

  1. 类型变量(如 T)可以用 extends 关键字进行绑定,表示 T 应该是绑定类型的子类型,但无法使用 super 关键字。
  2. 对于通配符 ?,可以用 extends 和 super 关键字进行限定,也可以使用无限定的通配符。
  3. 对于使用 extends 关键字限定的和无限定的通配符,可以作为返回值,但不能作为传入的参数类型
  4. 对于使用 super 关键字限定的的通配符则正好相反,不能作为返回值,但可以作为传入的参数类型
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值