Java 泛型 ? extends 与 ? super

本文详细介绍了Java中泛型的extends和super关键字的使用,它们分别用于只读(消费)和只写(生产)场景,提供了泛型的协变和逆变支持。通过举例说明,阐述了extends用于方法返回值,super用于方法参数的规则,以及在实际编程中的应用场景,如消费者接口和函数式接口。理解这些概念有助于提升代码的可复用性和框架设计的灵活性。
摘要由CSDN通过智能技术生成

我们经常在集合的泛型中用到 extends、super 关键字。先看下 List 集合中获取和放入接口的定义:
在这里插入图片描述
在这里插入图片描述
通过类定义可以看到,泛型的具体类型在创建集合实例时指定,用于限定该实例的 get/set 取出和放入时的集合元素类型。

  • List<? extends T>:声明上界,表示参数化的类型可能是所指定的 T 类型,或者是此类型的任意子类型。最终子类型:未知。
  • List<? super T>:声明下界,表示参数化的类型可能是所指定的 T 类型,或者是此类型的任意父类型。最终父类型:已知——Object。
  • Java 中泛型不变:假设有 A extends B,但 List<A> 和 List<B> 不存在型变关系。

泛型的简单使用

了解上述后,再看下面你就不会觉得奇怪。

  • 泛型不变
class A { }
class B extends A { }
List<A> list1 = new ArrayList<A>();	// work 泛型不变
list1.add(new A());     // work 
list1.add(new B());     // work 
A a = list1.get(1);		// work 
List<A> list2 = new ArrayList<B>();	// 编译错误,泛型不变,也就不支持协变(类似多态)

集合可读可写,集合泛型不变。

  • extends 泛型协变
class A { }
class B extends A { }

List<? extends A> list1 = new ArrayList<B>();	// 协变——父类引用指向子类
list1.add(new Object());  // 错误,容器不可写,不能放入任何值(null 除外)
A a = list1.get(1);	// work 可读,且有泛型

集合可读、不可写,集合泛型协变。

  • super 泛型逆变
class A {}
class B extends A {}

List<? super B> list = new ArrayList<A>();	// 逆变——子类引用指向父类
list.add(new A());	  // 编译错误,集合中放入的元素类型只能为 B 及 B 子类型
list.add(new B());    // work
Object b = list.get(0);  // work 可读,但无类型都是 Object

集合可读 Object 、可写,集合泛型逆变。

小结

在上面的例子中,我们至少能看出:

  • ? extends T 针对返回值泛型使用(如,只读的消费者集合泛型),指定的 T 为集合元素的通用父类型,用于限定取出类型为 T 的子类型、打破泛型不变
  • ? super T 针对方法参数泛型使用(如,只写的生产者集合泛型),指定的 T 为集合元素的通用父类型,用于限定放入类型为 T 的子类型、打破泛型不变

extends 与 super 互补。

extends 用于方法返回值,super 用于方法参数。即,我们所说的 PECS 原则。针对方法返回值即消费,针对方法参数即生产。

至于原理,编译器通过 ? super T 中关键字 super 得出本次协变泛型只作用在方法参数上。

因此,你的你若调用 List<? extends T> 的 get 方法,用到了不应该使用的方法 返回值,编译器将报错。

编译器通过 ? extends T 中关键字 extends 得出本次协变泛型只作用在方法返回值上。

因此,你的你若调用 List<? extends T> 的 add 方法,用到了不应该使用的方法参数,编译器将报错。

逆变协变优点

我们用 Java 对现实世界的水果进行简单的抽象,水果抽象为 Fruit,Apple等于 Fruit 存在继承关系。盛放水果的盘子 plate 被抽象为 List。

于是我们 OOP 代码抽象得到:

class Fruit {
}

class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Watermelon extends Fruit {
}

List<Fruit> plate1 = new ArrayList<Apple>();      // 编译错误
List<Fruit> plate2 = new ArrayList<Banana>();     // 编译错误
List<Fruit> plate3 = new ArrayList<Watermelon>(); // 编译错误
编译错误:

Java 中类型存在协变关系:List = ArrayList。

但是 Java 中类型上泛型不存在协变关系,即 List<Fruit> != ArrayList<Apple>,因此编译器提示泛型协变的编译错误。

解决方法:

我们利用上面学到的 ? extends 、? super 打破泛型不变的特性,提供泛型协变,提高代码的复用性:

class A {}
class B extends A{}
class C extends A{}

// 协变,用作只读型容器————集合中元素都是 A 或 A 子类型
List<? extends A> plate1 = new ArrayList<A>();
List<? extends A> plate2 = new ArrayList<B>();
List<? extends A> plate3 = new ArrayList<C>();
// 逆变,用作只写型容器————集合中元素都是 B 或 B 父类型
List<? super B> plate4 = new ArrayList<A>();

优点

? extends、? super 为带泛型的的类型提供了协变支持,提升了代码的可复用性

泛型协变:使得父类泛型可以引用子类泛型。

泛型逆变:使得子类泛型可以引用父类泛型。

通过上面的协变例子,

List<? extends Fruit> plate1 = new ArrayList<Apple>();

我们可以看出,List<? extends Fruit> 比直接使用 ArrayList<Apple> 更加通用。

因此我们将在 JDK 源码中,以及一些具有优良设计的第三方框架中能经常看到 ? extends、? super 的身影。

可复用性将使得框架去除臃肿显得更加精巧,以及提升可扩展性。

学习要本着大胆猜测,小心验证的原则。

super 在 JDK 中方法参数上的应用

比如 JDK8 中的消费者接口,用上了 ? super 进行方法的参数逆变

在这里插入图片描述

andThen 用于链式添加 Consumer 处理参数——针对参数即生产者,因此可以用 super。

那为什么要用 super ?直接写 T 不也行吗?
通过 ? super T 改变 Consumer 泛型从 accept 方法只接受 T 类型变为接受 T 及 T 的父类型。

优点见下面的例子:

class A {}
class B extends A{}
public static void main(String[] args) {
	new Consumer<B>() {
		@Override
		public void accept(B name) {
			System.out.println(name);
		}
	}.andThen(new Consumer<A>() {    // 逆变,泛型参数通用性提升
		// 如果 andThen(Consumer<T> after),由于泛型不变定理那这里只能接受 B 类型
		// super 逆变后 accept 除了 B 类型还 B 的父类型 A
		@Override
		public void accept(A s) {
			System.out.println(s);
		}
	});
}

于是,andThen Consumer#accept 参数类型从原本只能接受 B 类型到现在可以接受 B 的父类型 A 了。

extends 在 JDK 中方法返回值上的应用

再看具有生产与消费能力的接口:Function —— 具备输入输出函数。? extends 与 ? super 消费生产协变都用上了:

在这里插入图片描述

用在参数上用,也有在返回值上用——即生产者与消费者,因此 extends 与 super 都用上了。

这个的优点就自己探究吧。
提示:多态;对一个带参数的函数进行调用,本质上这个函数的参数声明作为父类引用,传入的参数对象可以是父类的任意子类实例。即,再次思考参数泛型关键字为什么是super,理解后应该很好记忆。

解析:被 super 修饰的泛型支持逆变,放在这里是因为该类型是被作为父类引用使用为目的的,Java 原本支持协变,再加上 super 抹除了不能往函数传入参数类型父类的限制,于是参数类型就变成了参数可传入 Object 的任意子类型。在 compose 的函数参数返回值上加上对函数参数返回值限定为 apply 函数的返回值的子类型。最终变成了,在 apply 时具有的参数只能协变的限制在 compose 传参的时候消失了,在 apply 时具有的返回值只能协变的限制在 compose 返回返回值时仍然存在。
注:compose 先于 apply 执行,compose 的参数 before 的 <V> 类型来自于编译器的类型推断,来自于等号左边的类型(任意类型)。compose 的参数 before 的返回值类型被限定为外层 apply 的协变类型。最终巧妙的实现了任意类型输入 compose 中先进行计算,再将被限定的计算得出的结果再放入 apply 中进行计算,最终得出最开始想要的 R 类型结果。
注:由于在进行 compose 时没有了输入类型限定,只有对结果进行限定以实现能接下来能链式调用 apply,于是便能对函数进行无限地 compose 操作。

小结

综上,若是我们想要学习框架源码,弄明白 ? extends、? super 是不可避免的。若是想要封装自己的框架, ? extends、? super 也是要熟练使用。

通过本文的学习,可以发现对于理解 ? extends、? super 还是有一定的心智负担。对于我们普通开发来说,也为了团队代码的可读性,可以直接使用具体类型,无需一定要使用 ? extends、? super 来写代码。

简单来说,就是减少不必要的炫技。

在函数式接口中的逆变协变可能稍显复杂,但在集合中使用的情况下只需要记住 extends 消费、super 生产即可。

补充

自限定类型
class SelfBounded<T extends SelfBounded<T>> {

    T element;
    
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    
    T get() { return element; }
}

SelfBounded 类接受泛型参数 T,而 T 由一个边界限定,这个边界就是拥有 T 作为其参数的SelfBounded。

作用:保证子类对基类成员或函数参数的重写。
示例:调用set(T arg),传入的参数必定是SelfBounded的子类,而不能是SelfBounded。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值