前言
泛型中的类型参数变量
T、K
等经常在代码中使用
?
即泛型中使用的通配符,在阅读源码中常会出现如Collection<? extends E> c
这样的表示,这样表示什么意思呢?为什么有时候插入数据的时候会报错呢?
类型参数变量和通配符的区别
此时想输出苹果集合,如果采取以下写法无法满足要求:
// 打印集合 public static void print(List<Fruit> list) { for (Fruit fruit : list) { System.out.println(fruit); } } List<Apple> appleList = new ArrayList<>(); Apple apple1 = new Apple(); Apple apple2 = new Apple(); appleList.add(apple1); appleList.add(apple2); print(appleList); //报错,因为此时的类型参数变量T=Fruit,所以list只能装有Fruit类型的元素
可以使用以下两个方法解决上述问题:
- 使用泛型
T
的extends
public static <T extends Fruit> void print(List<T> list) { for (T fruit : list) { System.out.println(fruit); } }
- 使用通配符
public static void print(List<? extends Fruit> list) { for (Fruit fruit : list) { System.out.println(fruit); } }
上述例子展现出了通配符
?
和类型参数变量T
是存在一定区别的:
- 两者使用
extends
的位置不同<T super Fruit>
会报错,而<? super Fruit>
不会
泛型通配符的使用和作用
泛型通配符主要解决以下两个需求(对于使用了上界或下界的通配符的读取和插入是有限定的,后续会具体说明):
从一个泛型集合里面读取元素
往一个泛型集合里面插入元素
要定义一个使用了泛型的集合,可以有以下三种方式:
List<?> genericsUnbounded = new ArrayList<>(); List<? extends A> genericsUpperbounded = new ArrayList<>(); List<? super A> genericsLowerbounded = new ArrayList<>();
Tips:
- 后续提到的
B
为A
的子类- 不知道集合是哪种类型(
List<A>
和List<B>
属于不同类型并不是因为集合中持有的元素一个是A
一个是B
),则集合所持有的元素类型也就不确定,所以不能随便向集合添加元素
无限定通配符
无限定即没有加上
extends
和super
关键字,比如List<?> genericsUnbounded
,它的意思是这个集合是一个可以持有任意类型的集合
- 由于可以是任意类型的集合,导致不知道集合具体是哪种类型,所以只能够对集合进行读操作:
List<?> genericsUnbounded = new ArrayList<>(); genericsUnbounded.add("psj"); // 报错 genericsUnbounded.get(0); // ok
- 当在函数的形参中使用无限定通配符时,除了在函数内部不能进行写操作,读取的时候应该直接使用
Object
类型接收元素:// read函数 public void read(List<?> elements){ for(Object o : elements){ System.out.println(o); } } // 传入任何类型的集合都可 List<A> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); read(list1); read(list2);
上界通配符
上界即加上
extends
关键字,比如List<? extends A> genericsUpperbounded
代表了一个可以持有A
及其子类实例的List
集合
- 尽管确定了集合中的元素是
A
或A
的子类,但依旧只能够对集合进行读操作,因为无法确定插入元素的类型List<? extends A> genericsUpperbounded = new ArrayList<>(); B a = new B(); // 假设原本想让genericsUpperbounded全部存储C类元素,此时想添加一个B类元素肯定不符合自己的要求,干脆直接报错 genericsUpperbounded.add(a); // 报错 genericsUnbounded.get(0); // ok
- 当在函数的形参中使用上界通配符时,除了在函数内部不能进行写操作,读取的时候可以使用
A
或Object
类型接收元素:// read函数 public void read(List<? extends A> elements){ for(A a : elements){ System.out.println(a); } } // 只能传入A或A的子类元素集合 List<A> list1 = new ArrayList<>(); List<B> list2 = new ArrayList<>(); List<String> list3 = new ArrayList<>(); read(list1); read(list2); read(list3); // 报错
下界通配符
下界即加上
super
关键字,比如List<? super A> genericsLowerbounded
代表了一个可以持有A
及其父类实例的List
集合
- 因为确定了集合中能包含的元素是
A
或A
的父类,所以可向集合中插入A和A的子类元素List<? super A> genericsLowerbounded = new ArrayList<>(); genericsLowerbounded.add(new A()); // 假设原本想让genericsLowerbounded全部存储A类元素,此时想添加一个B类元素,根据Java的多态是可以添加的进集合的 genericsLowerbounded.add(new B());
- 当在函数的形参中使用下界通配符时,在函数内部进行取操作时得到的元素必须为
Object
类型,因为无法确定是A
类还是其父类元素:// insert函数 public void insertElements(List<? super A> list) { list.add(new A()); list.add(new B()); A a = list.get(0); // 报错 } // 只能传入A或A的父类元素集合 List<A> list1 = new ArrayList<>(); List<Object> list2 = new ArrayList<>(); List<B> list3 = new ArrayList<>(); insert(listA); insert(listObject); insert(list3); // 报错