简述
通配符起到保护的作用,防止误操作
,通过idea等编辑器,能够检查出所有的通配符错误!
大家在平时的工作学习中, 肯定会见过不少如下的语句:
List<? super T>
List<? extends T>
我们都知道, 上面的代码时关于 Java 泛型的, 那么这两个不同的写法都有什么区别呢?
首先, 说到 Java 的泛型, 我们必须要提到的是Java 泛型的类型擦除机制
: Java中的泛型基本上都是在编译器这个层次来实现的. 在生成的 Java 字节代码中是不包含泛型中的类型信息的. 使用泛型的时候加上的类型参数, 会被编译器在编译的时候去掉. 这个过程就称为类型擦除. 如在代码中定义的List<Object>
和List<String>
等类型, 在编译之后都会变成List, JVM看到的只是List, 而由泛型附加的类型信息对JVM来说是不可见的.
在使用泛型类时:
-
我们可以使用一个具体的类型, 例如可以定义一个
List<Integer>
的对象, 我们的泛型参数就是Integer
; -
我们也可以使用
通配符 ?
来表示一个未知类型, 例如List<?>
就表示了泛型参数是某个类型, 只不过我们并不知道它的具体类型时什么.List<?> list= new ArrayList<Object>(); //ok
List<?>
所声明的就是所有类型都是可以的, 但需要注意的是,List<?>
并不等同于List<Object>
. 对于List<Object>
来说, 它实际上确定了 List 中包含的是 Object 及其子类, 我们可以使用 Object 类型来接收它的元素. 相对地,List<?>
则表示其中所包含的元素类型是不确定, 其中可能包含的是 String, 也可能是 Integer. 如果它包含了 String 的话, 往里面添加 Integer 类型的元素就是错误的. 作为对比, 我们可以给一个List<Object>
添加 String 元素, 也可以添加 Integer 类型的元素, 因为它们都是 Object 的子类.List<?> list= new ArrayList<?>(); //编译错误,右侧必须指定一个具体的类型,不能用?
正因为类型未知, 我们就不能通过
new ArrayList<?>()
的方法来创建一个新的ArrayList 对象, 因为编译器无法知道具体的类型是什么. 但是对于List<?>
中的元素, 我们却都可以使用 Object 来接收, 因为虽然类型未知, 但肯定是Object及其子类.-
等号左边可以声明 “?”,并且将来可以用Object类型来接收
-
右边必须指定一个确定的类型,不能用“?”代替 。
List<?> list= new ArrayList<Integer>(); //ok,右侧声明一个Integer Object o = list.get(0); //编译通过 Integer o2 = list.get(0); //编译失败,不能用Integer来接收
-
我们在上面提到了 List<?>
中的元素只能使用 Object 来引用, 这样作肯定时不太方便的, 不过幸运的是, Java 的泛型机制允许我们对泛型参数的类型的上界和下界做一些限制, 例如
List<? extends Number>
定义了泛型的上界是 Number, 即 List 中包含的元素类型是 Number 及其子类.- 而
List<? super Number>
定义了泛型的下界, 即 List 中包含的是 Number 及其父类.
当引入了泛型参数的上界和下界后, 我们编写代码相对来说就方便了许多, 不过也引入了新的问题, 即我们在什么时候使用上界, 什么时候使用下界, 以及它们的区别和限制到底时什么? 下面我来说说我的理解.
1. ? extends T
? extends T 描述了通配符上界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的子类, 例如:
List<? extends Number> numberArray = new ArrayList<Number>(); // Number 是 Number 类型的
List<? extends Number> numberArray2 = new ArrayList<Integer>(); // Integer 是 Number 的子类
List<? extends Number> numberArray3 = new ArrayList<Double>(); // Double 是 Number 的子类
上面三个操作都是合法的, 因为 ? extends Number 规定了泛型通配符的上界, 即我们实际上的泛型必须要是 Number 类型或者是它的子类, 而 Number, Integer, Double 显然都是 Number 的子类(类型相同的也可以, 即这里我们可以认为 Number 是 Number 的子类).
? extends T语法,由于无法确定子类型,可能是Integer,那么写入一个Double合适吗?不合适;可能是Double,那么写入一个Integer合适吗?不合适。因此就索性禁止写入了,但是由于知道上界,可以按照 T类型来读
1.1 关于读取,可读
Number o = numberArray.get(0); //ok
Integer o2 = numberArray2.get(0); //compile error
Double o3 = numberArray3.get(0); //compile error
根据上面的例子, 对于 List<? extends Number> numberArray
对象:
-
我们能够从 numberArray 中读取到 Number 对象, 因为 numberArray 中包含的元素是 Number 类型或 Number 的子类型.
-
我们不能从 numberArray 中读取到 Integer 类型, 因为 numberArray 中可能保存的是 Double 类型.
-
同理, 我们也不能从 numberArray 中读取到 Double 类型.
当然,我们这里讨论的是泛型,即不手动强转,下列语法展示手动强转,可以解决编译错误,但是存在运行时错误:
Integer o2 = (Integer)numberArray2.get(0); //compile ok ,run ok
Double o3 = (Double)numberArray2.get(0); //compile ok,run error,把numberArray2 Integer类型强转为Double,会在运行时报错
1.2 关于写入,不可写
List<? extends Number> numberArray2 = new ArrayList<Integer>(); //ok, Integer 是 Number 的子类
List<? extends Number> numberArray3 = new ArrayList<Double>(); //ok, Double 是 Number 的子类
numberArray.add(new Integer(0)); //compile error
numberArray2.add(new Double(0)); //compile error
根据上面的例子, 对于 List<? extends Number> numberArray
对象:
-
我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是
List<Double>
类型 -
我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是
List<Double>
类型 -
我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是
List<Integer>
类型
即, 我们不能添加任何对象到 List<? extends T> 中, 因为我们不能确定一个 List<? extends T>
对象实际的类型是什么, 因此就不能确定插入的元素的类型是否和这个 List 匹配. List<? extends T>
唯一能保证的是我们从这个 list 中读取的元素一定是一个 T 类型的.
2. ? super T
? super T
描述了通配符下界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的父类, 例如:
List<? super Integer> array = new ArrayList<Integer>(); //ok
List<? super Integer> array2 = new ArrayList<Number>();//ok
List<? super Integer> array3 = new ArrayList<Object>();//ok
? super T语法,由于无法确定父类型,因此法无法读定义 A a = array.get(0), (即期望能自动转换的,而不是手动进行类型转换的),但是由于知道下界,可以按照 T来写入
2.1 关于读取
List<? super Integer> array = new ArrayList<Integer>(); //ok
List<? super Integer> array2 = new ArrayList<Number>();//ok
List<? super Integer> array3 = new ArrayList<Object>();//ok
Integer o = array.get(0); //compile error
Integer o2 = array.get(0); //compile error
Integer o3 = array.get(0); //compile error
对于上面的例子中的 List<? super Integer> array 对象:
-
我们不能保证可以从 array 对象中读取到 Integer 类型的数据, 因为 array 可能是 List 类型的.
-
我们不能保证可以从 array 对象中读取到 Number 类型的数据, 因为 array 可能是 List 类型的.
-
唯一能够保证的是, 我们可以从 array 中获取到一个 Object 对象的实例.
2.2 关于写
List<? super Integer> array = new ArrayList<Integer>(); //ok
List<? super Integer> array2 = new ArrayList<Number>();//ok
List<? super Integer> array3 = new ArrayList<Object>();//ok
array.add(new Integer(0)); //ok
array2.add(new Double(0)); //compile error
array3.add(new Object()); //compile error
对于上面的例子中的 List<? super Integer> array 对象:
-
我们可以添加 Integer 对象到 array 中, 也可以添加 Integer 的子类对象到 array 中.
-
我们不能添加 Double/Number/Object 等不是 Integer 的子类的对象到 array 中.
3. 使用场景
PECE 原则: Producer Extends, Consumer Super
从上述两方面的分析,总结PECS原则如下:
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
- 如果既要存又要取,那么就不要使用任何通配符。
例子,Collections的源码,在copy方法中的定义,通配符起到保护的作用,防止误操作
:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src)
{
for (int i=0; i<src.size(); i++)
dest.set(i,src.get(i));
}
}