关于java泛型通配符的一些知识点

为什么会出现 通配符,它的出现是为了解决什么问题 为了内容的贯穿性,我先定义几个对象,这里有几个继承关系

 

scala

代码解读

复制代码

//水果 class Fruit{ public void out(){ System.out.println("水果顶级类"); } } //苹果 class Apple extends Fruit{ @Override public void out(){ System.out.println("苹果"); } } //中国苹果 class CnApple extends Apple{ @Override public void out(){ System.out.println("中国苹果"); } } //日本苹果 class JpApple extends Apple{ @Override public void out(){ System.out.println("日本苹果"); } } //橙子 class Orange extends Fruit{ @Override public void out(){ System.out.println("橙子"); } }

数组的转型

我们先看一下java数组,我们执行可以发现,我们将Apple类型的数组向上转型为Fruit类型的数组,然后往fruits数组里面存放一个Apple元素和CnApple元素(没有问题)。当我们将一个Fruit类型的对象存放进fruitsz数组时,编译器认为是没有问题的,但实际上在运行时是会报错的,因为这个fruits数组的实际类型是Apple类型,编译器并不知道fruits数组的实际类型。所以这个异常是被延迟到了 运行时期 而不是在 编译时期 就被捕获到,而泛型的作用就是将错误从运行时期提前到编译时期

 

scss

代码解读

复制代码

Fruit[] fruits = new Apple[10]; //没有问题,数组是完全在java语言中定义的,数组有内建的协变类型(也就是能够进行向上转型) fruits[0] = new Apple(); fruits[1] = new JpApple(); try{ fruits[2] = new Fruit(); //运行时期会报错 ArrayStoreException }catch (Exception e){ System.out.println(e); }

现在看一下泛型是怎么处理这个问题的,我们先定义两个List集合,如果把templist_02赋值给templist_01的引用,编译器是不允许你这样做的

 

ini

代码解读

复制代码

List<Fruit> templist_01 = new ArrayList<>(); List<Apple> templist_02 = new ArrayList<>(); templist_01 = templist_02; //报错

这里对于一些新人来说可能存在一定理解误区,虽说List和List在运行时期类型 擦除 都变成List。但是在编译时期编译器看来这两个就是List和List,这里的继承关系我们不能看集合元素的关系,我们应该把List和List都看作是一个个体,就是说 Fruit类型的List 和 Apple类型的List 是不一样的。为了达到在两种类型之间建立某种向上转型的关系,就有了通配符,通配符就是这个作用。

通配符是用 ?问号表示,然后根据实际需要,选择合适的用法

  1. <? extends T>
  2. <? super T>

=================分割线=====================

什么是<? extends T>

我们先讲讲<? extends T>,先说结论,这个在java泛型中被定义为“上界”,对于上界,它只能读不能写。直接上代码

整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

ini

代码解读

复制代码

//编译报错,因为apples_01只支持集合的元素类型是Apple及其子类,这里加了一个Fruit对象就不行 //List<? extends Apple> apples_01 = Arrays.asList(new Apple(),new Fruit()); List<? extends Apple> apples_01 = new ArrayList<>(); List<Fruit> fruits = new ArrayList<>(); apples_01 = fruits; //报错

在这里我们定义了一个只能支持元素类型是Apple及其子类的集合,List<? extends Apple> 可以理解为 "一个具有任何从 Apple继承的类型的列表"。我们在读取元素的时候是没问题的,至少可以知道它是一个Apple类型的,但是当我们想添加一个元素的时候,不管是不是Apple还是它的子类对象都无法写入(非null都无法写入)。编译器不允许我们这样做,因为编译器并不知道List<? extends Apple>对象具体引用的实际类型是什么?这个实际类型可以是 Apple、CnApple或者JpApple。

 

ini

代码解读

复制代码

List<? extends Apple> apples_01 = new ArrayList<>(); Apple apple = apples_01.get(0); //apples_01.add(new Apple()); //编译器报错 //apples_01.add(new CnApple()); //编译器报错 apples_01.add(null) //编译器允许这样但是加一个null没有实际意义

如果编译器允许写入就可能会出现类型转换错误的异常,考虑下面这种情况

 

ini

代码解读

复制代码

List<CnApple> apples_cn = Arrays.asList(new CnApple()); List<? extends Apple> apples_01 = new ArrayList<>(); apples_01 = apples_cn; Apple apple1 = apples_01.get(0); //没有问题 //假设编译器允许 <? extends T> 添加元素 //此时我们就在apples_cn的列表中加了一个日本的苹果,请注意我的描述是在【apples_cn】的列表,因为apples_01是对apples_cn列表对象的引用,实际修改的是apples_cn apples_01.add(0, new JpApple()); //这是我们假设的,实际编译器不允许这么做 Apple temp1 = apples_01.get(0); //没问题 CnApple temp2 = apples_cn.get(0) //报错

如果我们执行apples_01.get(0),这是没有问题的,拿到的元素类型是Apple(注:编译器仅能能知道元素类型是Apple类型,你可以根据自己知道的实际类型进行强转,此时apples_01.get(0)的实际类型JpApple)。

但是此时如果我们执行apples_cn.get(0),就有问题了。因为这个执行返回的CnApple(中国苹果)类型,但是这个实际是我们放进去的JpApple(日本苹果),那么就会将JpApple强转为CnApple,显然这里强转是不行的。所以说编译器为了避免这种在运行时期类型转换可能带来的错误,就不允许进行写数据。

至于为什么叫上界,是因为 <? extends T> 限制了类型最多只能是T类型,最少必须要是T类型的子类,超过T类型就不行。

=================分割线=====================

什么是<? super T>

再讲一下 <? super T>,为什么会有这个呢?在了解<? extends T>的特点之后我们知道它只能读不能写,<? super T>就会为了解决不能写的问题。但是<? super T>并不是作为 <? extends T>的“增强”,这两者其实是一个互补的关系。

先说结论,<? super T>在java泛型中被定义为“下界”,对于下界,它只能写不能读【请注意,这里说的不能读不是真正意义上的无法读,后面有做解释】

List<? super T> 我们可以读作 “一个具有T类型或者T类型父类的列表”,至少必须是T类型,这就是叫“下界”的原因

 

ini

代码解读

复制代码

List<? super Apple> apples = new ArrayList<>(); apples = new ArrayList<Fruit>(); //没问题 apples = new ArrayList<CnApple>(); //编译器报错

我们可以看到,将Apple父类Fruit类型的列表赋值给<? super Apple>列表的引用是没有问题的。但当我们尝试把Apple子类CnApple类型的列表赋值给<? super Apple>列表的引用时编译器就会报错。说明了<? super T>只能接受T类型及其父类型

写数据只能是T类型及其子类型

 

csharp

代码解读

复制代码

List<? super Apple> apples = new ArrayList<>(); apples.add(new Apple()); apples.add(new CnApple()); apples.add(new Fruit()); //编译报错

那为什么只能写不能读呢?因为对于<? super T>,它的具体类型是明确的,要么是T类型,要么是T类型的父类型,而写数据时只能是T类型及其子类型,根据多态的向上转型是允许这样做的,所以写数据是没有问题的。不能读是因为不知道具体的引用类型是什么,但不是说真的不能读。不能直接读到T类型,读出来的对象只能是Object类型的。

 

ini

代码解读

复制代码

List<? super Apple> apples = new ArrayList<>(); List<Fruit> fruits = Arrays.asList(new Fruit()); apples = fruits; Object o = apples.get(0); //编译器只能知道Object类型 System.out.println(o.getClass().getSimpleName()); // Fruit

为什么只能是Object类型呢?因为编译不知道这个引用究竟是T类还是T类的父类。下面的代码就说明了这一点。假如直接用T类型,就会报错。这里apples.get(0)的实际类型是Fruit,Fruit是Apple的父类。 我们可以说“苹果是水果”,但是不能说“水果是苹果”。这就是为什么只能读到Object类型的原因

 

ini

代码解读

复制代码

List<? super Apple> apples = new ArrayList<>(); List<Fruit> fruits = Arrays.asList(new Fruit()); apples = fruits; Apple o = apples.get(0); //报错

以上就是关于泛型通配符的知识点,请记住,不管是泛型还是泛型通配符,它们的作用都是为了在编译期起到检查的作用,将异常从 运行期 提前到 编译期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值