List extends T与List super T的区别
List<? extends T>与 List<? super T>的区别
List<? extends T>与 List<? super T>的区别
这个问题,初级工程师在做面试题时会出现。高级工程师很少会遇到,但实际上,不少工作了 5 年左右的工程师也回答的不是很好。基于此,整理了本文,分享给大家!
我们先来看一下名词解释:
1)?
?表示类型通配符,即具体传什么参数类型,在 List 定义时不用考虑。
2)<T>
这里的 <>
表示泛型,T 表示泛型中装载的类型为 T 类型,等到需要的时候,我们可以具体这个 T。我们在使用动态数组实现 ArrayList 的时候,如果希望这个 ArrayList 不仅仅支持一个类型的话,我们可以给这个 ArrayList 定义泛型,泛型中存放的是 T 类型。在实际创建出这个 ArrayList 对象时,我们可以指定泛型中的具体类型。
3)<? extends T>
类型上界,这里的 ? 可以是 T 类型或者 T 的子类类型。
4)<? super T>
类型下界,这里的?可以是 T 类型或者 T 的超类类型,但不代表我们可以往里面添加任意超类类型的元素。
在 List 中引入通配符界限限制的假设
不管是List<? extends T>
还是List<? super T>
,如果能读取元素,那么这个元素一定能转化为 T 类型,注意不是强制类型转换,强制类型转换是容易出现问题。
显然List<? extends T>
内都是 T 的子类类型,能够向上转型为 T 类型,因此该 list 可以读取。
而List<? super T>
内可以是 T 的超类类型,T 的超类转 T 是有可能出现异常的。
那我干脆转化成 Object 类型不好吗,所有类的基类都是 Object,不属于强制类型转换。哥们,转换成 Object 了,那你还图个啥?转换为 Object 类型是没有意义的。
假设List<? extends T>
能添加元素,那么需要满足添加的任意元素需要能够直接转化成 T 的任何一个子类,T 的子类 A 和子类 B 是不能相互转化的,显然该 list 是不能添加元素的。
假设List<? super T>
能添加元素,那么同样需要满足添加的任意元素能够直接转化成 T 的任何一个超类。此时添加 T 的子类元素就能满足该要求,因为 T 的任意子类可以向上转型成 T 的任何超类。
List<? extends T>
List<? extends T>
是被设计用来读取数据的泛型,并且只能读取类型为 T 的元素。原因如下:
元素是可以进行向上转型的,因此,我们可以这样做来读取元素。
List<? extends Number> list = new ArrayList<>();
Number number = list.get(0);
可以读取,但不能写入,比如以下的代码就直接报错。
public class Main {
static class A { }
static class B extends A { }
static class C extends A { }
public static void main(String[] args) {
List<? extends A> list = new ArrayList<>();
list.add(new A());//编译报错
list.add(new B());//编译报错
list.add(new C());//编译报错
}
}
A 的子类 B 与子类 C 是不能相互转换的,因此是不能往该 list 中添加元素。
虽然不能添加元素,但可以在初始化的时候,接受一个已经定义好的 list,而该 list 存放的类型一定相同。因此,List<? extends T>
可直接接受一个定义好的 list。
public static List<Integer> getList(){
List<Integer> list=new ArrayList<>();
list.add(1);
return list;
}
// ....
public static void main(String[] args) {
List<? extends Number> list = new ArrayList<>();
list=getList();
}
List<? super T>
List<? super T>
是被设计用来添加数据的泛型,并且只能添加 T 类型或其子类类型的元素。面试宝典:https://www.yoodb.com
为什么只能是 T 类型及其子类型元素,超类类型的元素不可以吗?
超类类型转化为 T 类型,是需要强制类型转换的,是容易出现异常的,无法保障的。
而传入 T 类型及其子类类型时,能够直接转化为 T 的任意超类类型。比如,下面的代码是可以运行的
public class Main {
static class A { }
static class B extends A { }
static class C extends A { }
public static void main(String[] args) {
List<? super A> list = new ArrayList<>();
list.add(new A());
list.add(new B());
list.add(new C());
}
}
该 list 也可以读取其中的元素,从第二节可以得出,只能用 Object 接收,没多大意义。
List<? super Integer> list2 = new ArrayList<>();
list2.add(new Integer(1));
Object integer=list2.get(0);
如果我们使用 Object 类型来接收获取到的元素,那么元素本身的类型就会丢失,因此,我们不使用List<? super T>
来获取元素。
如果我们非要使用List<? super Integer>
中的 Integer 类型来接收获取到的元素,那么必须进行强制类型转换,是会出现异常的,无法保障。
List<? super Integer> list2 = new ArrayList<>();
list2.add(new Integer(1));
Integer integer1= (Integer) list2.get(0);
总结
(1)List<? extends T>
适用于读取数据,读取出来的数据全部用 T 类型接收。如果我们往此 list 中添加 T 类型不同的子类的话,各种子类无法相互转换,因此不能添加元素,但可接受初始赋值。公众 号 Java 精选,回复 java 面试,获取面试资料,支持在线刷题。
(2)List<? super T>
适用于添加元素,只能添加 T 类型或其子类类型。因为这些类型都能转换为 T 的任意超类类型(向上转型),因此我们可以对此 list 添加元素。只能用 Object 类型来接收获取到的元素,但是这些元素原本的类型会丢失。
更加通俗易懂的例子
什么,你还没明白?那我举一些直观的例子。
注意:向上转型是安全的,向下转型是不安全的,除非你知道 List 中的真实类型,否则向下转型就会报错
。
extends
List<? extends Number> foo3
意味着下面的赋值语句都是合法的:
List<? extends Number> foo3 = new ArrayList<Number>(); // Number "extends" Number (in this context)
List<? extends Number> foo3 = new ArrayList<Integer>(); // Integer extends Number
List<? extends Number> foo3 = new ArrayList<Double>(); // Double extends Number
- 读取
给定上述可能的赋值语句,能保证你从List foo3
中取出什么样类型的对象?
- 你可以读取一个
Number
对象,因为上面任意一个 list 都包含Number
对象或者Number
子类的对象(上面的 Number、Integer、Double 都可以转型成 Number,并且是安全的,所以读取总是可以的)。如下代码就不会报错:
List<? extends Number> foo4 = new ArrayList<Integer>();
Number number = foo4.get(0);
- 你不能读取一个
Integer
对象,因为foo3
可能指向的是List<Double>
(与其运行时发现 Double 转成 Integer 报错,不如编译时就不让从foo3
中取Integer
对象)。如下代码编译时会报Incompatible types
错的:
List<? extends Number> foo4 = new ArrayList<Integer>();
Integer number = foo4.get(0);
因为编译的时候编译器只知道 foo4 引用是一个 List<? extends Number>,要到运行时才会绑定到 new ArrayList() ,所以编译的时候是无法判断 foo4 指向的 List 中到底是什么类型,唯一能确定的就是这个类型是 Number 的子类(或者就是 Number 类)。
- 你也不能读取一个
Double
对象,因为foo3
可能指向的是List<Integer>
。
- 写入
给定上述可能的赋值语句,你能往List foo3
中添加什么类型的对象从而保证它对于所有可能的ArrayList
都是合法的呢?
- 你不能添加一个
Integer
对象,因为foo3
可能指向的是List<Double>
。如下代码是会编译报错的:
List<? extends Number> foo4 = new ArrayList<Integer>();
foo4.add(new Integer(1));
因为编译期间是无法知道 foo4 指向的 ArrayList 中到底放的是什么类型,只有到运行时才知道(就是 Java 所谓的晚绑定或运行时绑定)。与其到运行时发现往一个 ArrayList 中 add 一个 Integer 导致抛出类型转换异常,倒不如编译时就报错,即使 ArrayList 中放的就是 Integer 类型。
- 你不能添加一个
Double
对象,因为foo3
可能指向的是List<Integer>
。 - 你不能添加一个
Number
对象,因为foo3
可能指向的是List<Integer>
。
「总结一下」:你不能往List<? extends T>
中添加任何对象,因为你不能保证List
真正指向哪个类型,所以不能确定添加的对象就是List
所能接受的类型。能保证的,仅仅是你可以从List
中读取的时候,你获得的肯定是一个T
类型的对象(即使是T
类型的子类对象也是T
类型的)。
supers
现在考虑List<? super T>
包含通配符的声明List<? super Integer> foo3
意味着下面任何一个赋值语句都是合法的:
List<? super Integer> foo3 = new ArrayList<Integer>(); // Integer is a "superclass" of Integer (in this context)
List<? super Integer> foo3 = new ArrayList<Number>(); // Number is a superclass of Integer
List<? super Integer> foo3 = new ArrayList<Object>(); // Object is a superclass of Integer
- 读取
给定上述可能的赋值语句,当读取List foo3
中的元素的时候,你能保证接收到什么类型的对象呢?
- 你不能保证是一个
Integer
对象,因为foo3
可能指向一个List<Number>
或者List<Object>
。 - 你不能保证是一个
Number
对象,因为foo3
可能指向一个List<Object>
。 - 你能保证的仅仅是它一定是一个
Object
类的实例或者Object
子类的实例(但是你不知道到底是哪个子类)。
- 写入
给定上述可能的赋值语句,你能往List foo3
中添加什么类型的对象从而保证它对于所有可能的ArrayList
都是合法的呢?
- 你可以添加一个
Integer
实例,因为Integer
类型对于上述所有的 list 都是合法的。 - 你可以添加任何
Integer
子类的实例,因为一个Integer
子类的实例都可以向上转型成上面列表中的元素类型。 - 你不可以添加
Double
类型,因为foo3
可能指向的是ArrayList<Integer>
。 - 你不可以添加
Number
类型,因为foo3
可能指向的是ArrayList<Integer>
。 - 你不可以添加
Object
类型,因为foo3
可能指向的是ArrayList<Integer>
。
PECS
PECS
是"Producer Extends,Consumer Super"(生产者用 Extends,消费者用 Super)的缩写。
- "Producer Extends"的意思是,如果你需要一个
List
去生产T
类型 values(也就是说你需要去 list 中读取T
类型实例),你需要声明这个List
中的元素为? extends T
,例如List<? extends Integer>
,但是你不能往里面添加元素。 - "Consumer Super"的意思是,如果你需要一个
List
去消费T
类型 values(也就是说你需要往 list 中添加T
类型实例),你需要声明这个List
中的元素为? super T
,例如List<? super Integer>
。但是不能保证你从这个 list 中读取出来对象类型。 - 如果你既需要往 list 中写,也需要从 list 中读,那么你就不能用通配符
?
,必须用精确的类型,比如List<Integer>
。 - 可以参考 JDK 源码中的 Collections 类的 copy 方法,来理解 PECS,源码在文末有。