Java 泛型 ? extends 与 ? super

我们经常在集合的泛型中用到 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 都用上了。

这个的优点就自己探究吧。

小结

综上,若是我们想要学习框架源码,弄明白 ? 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。

 
posted @ 2022-11-20 21:15  r1-12king  阅读(269)  评论(0编辑  收藏  举报