深入理解通配符

泛型通配符:

List<? extends Number> list = new ArrayList();
List<? super Number> list = new ArrayList();

java编译时类型和运行时类型

定义两个类,Apple继承Fruit

Class Fruit {
}
Class Apple extends Fruit {
}
// apple指向的对象在编译时和运行时都是Apple类型
Apple apple = new Apple();

Java的向上转型现象:

// apple指向的对象在编译时是Fruit类型,而在运行时是Apple类型
Fruit apple = new Apple();

原理:因为在编译的时候,JVM 只知道 Fruit 类变量指向了一个对象,并且这个对象是 Fruit 的子类对象或自身对象,其具体的类型并不确定,有可能是 Apple 类型,也有可能是 Orange 类型。而为了安全方面的考虑,JVM 此时将 apple 属性指向的对象定义为 Fruit 类型。因为无论其是 Apple 类型还是 Orange 类型,它们都可以安全转为 Fruit 类型。
而在运行时阶段,JVM 通过初始化知道了它指向了一个 Apple 对象,所以其在运行时的类型就是 Apple 类型。

泛型中的向上转型

public class Plate<T> {
    private List<T> list;
    public Plate(){} 
    public void add(T item){list.add(item);}
    public T get(){return list.get(0);}
}

我们定义了一个Plate类,泛型T的意思是这个类可以接受任何类型的对象,比如把Fruit放进去:

Plate<Fruit> plate = new Plate<Fruit>();

那么泛型尖括号可以向上转型吗?比如:

Plate<Fruit> plate = new Plate<Apple>();  //Error

会出现编译错误,因为泛型不支持直接向上转型,JVM会要求Fruit泛型指向的对象必须也是Fruit。
正是为了解决保持「向上转型」概念在 Java 语言中的统一,使泛型也支持向上转型,所以 Java 推出了通配符的概念:

// 限定上限
Plate<? extends Fruit> plate = new Plate<Apple>();

上面的这行代码表示:plate 可以指向任何 Fruit 类或Fruit 的子类对象。Apple 是 Fruit 的子类,自然就可以正常编译了。

extends通配符的缺陷

我们无法向Plate中添加对象,只能读取对象。
原理:上面我们对盘子的定义中,plate 可以指向任何 Fruit 类对象,或者任何 Fruit 的子类对象。也就是说,plate 属性指向的对象其在运行时可以是 Apple 类型,也可以是 Orange 类型,也可以是 Banana 类型,只要它是 Fruit 类,或任何 Fruit 的子类即可。即我们下面几种定义都是正确的:

Plate<? extends Fruit> plate = new Plate<Apple>();
Plate<? extends Fruit> plate = new Plate<Orange>();
Plate<? extends Fruit> plate = new Plate<Banana>();

这样子的话,在我们还未具体运行时,也就是在编译期,JVM 并不知道我们要往盘子里放的是什么水果,到底是苹果,还是橙子,还是香蕉,完全不知道。既然我们不能确定要往里面放的类型,那 JVM 就干脆什么都不给放,避免出错。
正是出于这种原因,所以当使用 extends 通配符时,我们无法向其中添加任何东西。
那为什么又可以取出数据呢?因为无论是取出苹果,还是橙子,还是香蕉,我们都可以通过向上转型用 Fruit 类型的变量指向它,这在 Java 中都是允许的,但必须用上限接收,因为不确定从中拿到的是什么东西:

Fruit apple = plate.get();
Apple apple = plate.get();  //Error

可以从上面的代码看到,当你尝试用一个 Apple 类型的变量指向一个从盘子里取出的水果时,是会提示错误的。
所以当使用 extends 通配符时,我们可以取出所有东西,但必须用上限接收。

super通配符的缺陷

与 extends 通配符相似的另一个通配符是 super 通配符,其特性与 extends 完全相反。

// 限定下限
Plate<? super Apple> plate = new Plate<Fruit>();

上面这行代码表示 plate 属性可以指向一个特定类型的 Plate 对象,只要这个特定类型是 Apple 或 Apple 的父类。也就是说,如果 EatThing 类是 Fruit 的父级,那么下面的声明也是正确的:

Plate<? super Apple> plate = new Plate<EatThing>();

当然了,下面的声明肯定也是对的,因为 Object 是任何一个类的父级。

Plate<? super Apple> plate = new Plate<Object>();

既然这样,也就是说 plate 指向的具体类型可以是任何 Apple 的父级,JVM 在编译的时候肯定无法判断具体是哪个类型。但 JVM 能确定的是,任何 Apple 的子类都可以转为 Apple 类型,但任何 Apple 的父类都无法转为 Apple 类型
所以对于使用了 super 通配符的情况,我们只能存入 T 类型及 T 类型的子类对象。

Plate<? super Apple> plate = new Plate<Fruit>();
plate.add(new Apple());
plate.add(new Fruit()); //Error

当我们向 plate 存入 Apple 对象时,编译正常。但是存入 Fruit 对象,就会报编译错误。
而当我们取出数据的时候,也是类似的道理。JVM 在编译的时候知道,我们具体的运行时类型可以是任何 Apple 的父级,那么为了安全起见,我们就用一个最顶层的父级来指向取出的数据,这样就可以避免发生强制类型转换异常了。

Object object = plate.get();
Apple apple = plate.get();  //Error
Fruit fruit = plate.get();  //Error

从上面的代码可以知道,当使用 Apple 类型或 Fruit 类型的变量指向 plate 取出的对象,会出现编译错误。而使用 Object 类型的额变量指向 plate 取出的对象,则可以正常通过。
也就是说对于使用了 super 通配符的情况,我们取出的时候只能用 Object 类型的属性指向取出的对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值