“<? extends T>和<? super T>“是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
“<? extends T>”:是指 上界通配符(Upper Bounds Wildcards)
“<? super T>”:是指 下界通配符(Lower Bounds Wildcards)
开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List<Object>
作为形式参数,那么如果尝试将一个List<String>
的对象作为实际参数传进去,却发现无法通过编译。
虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。
举个例子:
虽然Object 是 String 的父类,但是整体来说,容器List<Object >
并不是List<String>
的父类。
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系。
在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。
您也许认为这一原理同样适用于泛型类型 —— List< Number>
是List< Integer>
的超类型,那么可以在需要 List< Number>
的地方传递List< Integer>
。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>
和List<String>
等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是 Java 的泛型实现方式与C++ 模板机制实现方式之间的重要区别。
public class Test {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getName());
System.out.println(intList.getClass().getName());
}
}
上面这一段代码,运行后输出如下,可知在运行时获取的类型信息是不带具体类型的:
java.util.ArrayList
java.util.ArrayList
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
List<String>.class
或是 List<Integer>.class
,而只有 List.class,因此在运行时无法获得泛型的真实类型信息。new MyClass<String>
还是new MyClass<Integer>
创建的对象,都是共享一个静态变量。MyException<String>
和MyException<Integer>
的。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get();List< String> 就变成了 List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
当类型信息被擦除之后,上述类的声明变成了 class MyString implements Comparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口 Comparable 声明的 int compareTo(Object) 方法。这个时候就由编译器来动态生成这个方法。
了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List< Object> 和 List< String> 为例来具体分析:
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); // 这个操作在当前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); // 编译错误
}
这段代码中,inspect 方法接受 List 作为参数,当在 test 方法中试图传入 List 的时候,会出现编译错误。
假设这样的做法是允许的,那么在 inspect 方法就可以通过 list.add(1) 来向集合中添加一个数字。这样在 test 方法看来,其声明为 List 的集合中却被添加了一个 Integer 类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。
编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。
为了让泛型用起来更舒服,Sun的大师们就想出了<? extends T>和<? super T>的办法,来让”水果盘子“和”苹果盘子“之间发生正当关系。
在 Java 中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如 String 继承自 Object。根据Liskov 替换原则,子类是可以替换父类的。当需要 Object 类的引用的时候,如果传入一个 String 对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的(数组是协变的)。 String[] 可以替换 Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的 List<String>
是不能替换掉List<Object>
的。
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List 和 List 这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List 接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:
假设,我们有下面几个类:
class Box<T> {
public Box() {
}
private T item;
public Box(T t) {
item = t;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
}
class Food {
}
class Meat extends Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class RedApple extends Apple {
}
class GreenApple extends Apple {
}
泛型通常用于容器,假如我们有一些箱子:
可以装肉的箱子 new Box< Meat>();
可以装水果的箱子 new Box< Fruit>();
上界通配符(Upper Bounds Wildcards)
不能set值,只能get值,所以作为生产者,后面会说为什么
Box<? extends Food>
一个只能new Food以及一切是Food子类的箱子(小于等于关系)
(也就是只能new Food以及Food的子类)
Box<? extends Food> box1 = new Box<Food>();
Box<? extends Food> box2 = new Box<Fruit>();
Box<? extends Food> box3 = new Box<Meat>();
作为生产者Producer,可以取出
// 上界
Box<? extends Food> box1 = new Box<Food>(new Fruit());
Box<? extends Food> box2 = new Box<Fruit>(new Apple());
Box<? extends Food> box3 = new Box<Meat>(new Meat());
box1 只能get Food的实现类以及一切具体实现类(Food)子类的箱子
box2 只能get具体Food和Fruit之中小的那一个实现类以及一切具体实现类(Fruit)子类的箱子
box3 只能get具体Food和Meat之中小的那一个实现类以及一切具体实现类(Meat)子类的箱子
box1:上界是Food,new了一个new Box< Food>(),所以box1 可以get到的范围是 Food以及Food的子类
box2:上界是Food,new了一个new Box< Fruit>(),所以box2 可以get到的范围是 Fruit以及Fruit的子类
box3:上界是Food,new了一个new Box< Meat>(),所以box1 可以get到的范围是 Meat以及Meat的子类
相对应的下界通配符(Lower Bounds Wildcards)
不影响往里存,但往外取只能放在Object对象里,后面会说为什么
Box<? super Apple>
一个只能new Apple以及一切是 Apple父类的箱子(大于等于关系)
只能set具体Apple和Fruit之中小的那一个实现类以及一切具体实现类子类的箱子
Box<? super Apple> box11 = new Box<Fruit>();
Box<? super Apple> box22 = new Box<Food>();
小的那一个
box11 只能get或set具体Apple和Fruit之中小的那一个实现类以及一切具体实现类(Fruit)子类的箱子
box22 只能get或set具体Apple和Food之中小的那一个实现类以及一切具体实现类(Apple)子类的箱子
box11:下界是Apple,new了一个new Box< Fruit>(),Fruit>=Apple
box22:下界是Apple,new了一个new Box< Food>(),Food>=Apple
JAVA泛型通配符的使用规则就是赫赫有名的“PECS”(生产者使用“? extends T”通配符,消费者使用“? super T”通配符)。
从上述两方面的分析,总结PECS原则如下:
参考1:Java 泛型 <? super T> <? extend T> 的通俗理解
参考2:Java泛型解惑之 extends T>和 super T>上下界限
参考3:JAVA泛型通配符PECS原则Producer Extends Consumer Super