extends T>
和 super T>
是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收 List
虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。
比如我们有Fruit类,和它的派生类Apple
class Fruit {}
class Apple extends Fruit {}
然后有一个最简单的容器:Plate类,盘子里可以放一个泛型的”东西”
我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。
class Plate{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
现定义一个“水果盘”,逻辑上水果盘当然可以装苹果。
Plate p=new Plate(new Apple());
但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。
error: incompatible types: Plate cannot be converted to Plate
实际上,编译器认定的逻辑是这样的:
苹果 IS-A 水果
装苹果的盘子 NOT-IS-A 装水果的盘子
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系。
所以我们不可以把Plate
在 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 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 ListJVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是 Java 的泛型实现方式与C++ 模板机制实现方式之间的重要区别。
public class Test {
public static void main(String[] args) {
List strList = new ArrayList<>();
List intList = new ArrayList<>();
System.out.println(strList.getClass().getName());
System.out.println(intList.getClass().getName());
}
}
上面这一段代码,运行后输出如下,可知在运行时获取的类型信息是不带具体类型的:
java.util.ArrayList
java.util.ArrayList
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get();List
class MyString implements Comparable {
public int compareTo(String str) {
return 0;
}
}
当类型信息被擦除之后,上述类的声明变成了 class MyString implements Comparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口 Comparable 声明的 int compareTo(Object) 方法。这个时候就由编译器来动态生成这个方法。
了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List
public void inspect(List
这段代码中,inspect 方法接受 List
假设这样的做法是允许的,那么在 inspect 方法就可以通过 list.add(1) 来向集合中添加一个数字。这样在 test 方法看来,其声明为 List
编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。
为了让泛型用起来更舒服,Sun的大师们就想出了 extends T>和 super T>的办法,来让”水果盘子“和”苹果盘子“之间发生正当关系。
在使用泛型类的时候,
既可以指定一个具体的类型,如 List
也可以用通配符? 来表示未知类型,如 List> 就声明了 List 中包含的元素类型是未知的。
通配符所代表的其实是一组类型,但具体的类型是未知的。List> 所声明的就是所有类型都是可以的。但是 List> 并不等同于 List
List正因为类型未知,就不能通过 new ArrayList>() 的方法来创建一个新的 ArrayList 对象。因为编译器无法知道具体的类型是什么。但是对于 List> 中的元素确总是可以用 Object 来引用的,因为虽然类型未知,但肯定是 Object 及其子类。考虑下面的代码:
public void wildcard(List> list) {
list.add(1);// 编译错误
}
如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。
这就是三句话总结JAVA泛型通配符(PECS)中的第一句话:?”不能添加元素,只能作为消费者
因为对于 List> 中的元素只能用 Object 来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如 List extends Number> 说明 List 中可能包含的元素类型是 Number 及其子类。而 List super Number> 则说明 List 中包含的是 Number 及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List extends Number> 的时候,就可以使用 Number 类的 intValue 等方法。
在 Java 中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如 String 继承自 Object。根据Liskov 替换原则,子类是可以替换父类的。当需要 Object 类的引用的时候,如果传入一个 String 对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的(数组是协变的)。 String[] 可以替换 Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的 List
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List
理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把 List
下面就是上界通配符(Upper Bounds Wildcards)
Plate<? extends Fruit>
一个能放水果以及一切是水果派生类的盘子
再直白点就是:啥水果都能放的盘子,这和我们人类的逻辑就比较接近了
Plate<? extends Fruit>和Plate
直接的好处就是,我们可以用“苹果盘”给“水果盘”赋值了。
Plate extends Fruit> p=new Plate(new Apple());
再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。
//Lev 1
class Food{}
//Lev 2
class Fruit extends Food{}
class Meat extends Food{}
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
在这个体系中,上界通配符Plate<? extends Fruit>覆盖下图中蓝色的区域。
相对应的下界通配符(Lower Bounds Wildcards)
Plate super Fruit>
表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。
Plate<? super Fruit>是Plate
对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。
边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。
还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set( )新东西,以及从盘子里get( )东西。
class Plate{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
1、上界 extends T>不能往里存,只能往外取
(1). extends Fruit>会使往盘子里放东西的set( )方法失效,但取东西get( )方法还有效
(2).取出来的东西只能存放在Fruit或它的基类里面,向上造型。
比如下面例子里两个set()方法,插入Apple和Fruit都报错。
Plate extends Fruit> p=new Plate(new Apple());
//不能存入任何元素
p.set(new Fruit()); //Error
p.set(new Apple()); //Error
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get(); //Error
编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道,因此取出来的时候要向上造型为基类。
可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate
然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个capture#1匹配,所以就都不允许。
所以通配符>和类型参数
比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer...
public List fill(T... t);
但通配符>没有这种约束,Plate>单纯的就表示:盘子里放了一个东西,是什么我不知道。
2、下界 super T>不影响往里存,但往外取只能放在Object对象里
(1).使用下界 super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。
因为规定的下界,对于上界并不清楚,所以只能放到最根本的基类Object中
(2).set( )方法正常。
Plate super Fruit> p=new Plate(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。
既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。
但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。
最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了。
Producer Extends 生产者使用Extends来确定上界,往里面放东西来生产
Consumer Super 消费者使用Super来确定下界,往外取东西来消费
1、频繁往外读取内容的,适合用上界Extends,即extends 可用于的返回类型限定,不能用于参数类型限定。
2、经常往里插入的,适合用下界Super,super 可用于参数类型限定,不能用于返回类型限定。
3、带有 super 超类型限定的通配符可以向泛型对象用写入,带有 extends 子类型限定的通配符可以向泛型对象读取
参考:
深入理解 Java 泛型https://www.jianshu.com/p/ca12115bf92c
java泛型深度历险https://www.infoq.cn/article/cf-java-generics