JAVA 学习·泛型(二)——通配泛型

有关泛型的基本概念,参见我的前一篇博客 JAVA 学习·泛型(一)。

协变性

泛型不具备协变性

  在介绍通配泛型之前,先来看一下下面的例子。我们定义了一个泛型栈:

import java.util.ArrayList;
class GenericStack<E> {private ArrayList<E> list = new ArrayList<E>();public boolean isEmpty() {return list.isEmpty();}public int getSize() {return list.size();}public E peek() {return list.get(getSize() - 1);//取值不出栈}public E pop() {E o = list.get(getSize() - 1) ;list.remove(getSize() - 1);return o;}public void push(E o) {list.add(o);}public String toString() {return "stack: " + list.toString();}
}

  现在,我们写了一个方法max,用来求一个GenericStack容器中元素的最大值。如下面的代码所示:

public class WildCardNeedDemo {public static double max(GenericStack<Number> stack){double max = stack.pop().doubleValue();while (! stack.isEmpty()){double value = stack.pop().doubleValue();if(value > max)max = value;}return max;}public static void main(String[] args){GenericStack<Integer> intStack = new GenericStack<>();intStack.push(1);intStack.push(2);intStack.push(3);System.out.println("Th max value is " + max(intStack));}
}

  上面的main函数,意图在于借助WildCardNeedDemo.max方法,找出intStack中的最大值3。但是实际运行时,程序报错,说GenericStack<Integer>无法转换为GenericStack<Number>类型。这是因为泛型不具备协变性
  所谓的协变性在泛型中是指:有泛型类Generic<T>如果BA的子类,那么Generic<B>也是Generic<A>的子类。

数组具备协变性

  协变性在数组中是指:如果类A是类B的父类,那么A[]就是B[]的父类。数组具有协变性
  数组的协变性是 Java 开发者和使用者所公认的一个瑕疵,因为它会导致编译通过的地方运行时出错的问题。比如下面这个例子:

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{} //一种苹果
class Orange extends Fruit{}
//由于数组的协变性,可以把Apple[]类型的引用赋值给Friut[]类型的引用
Fruit[] fruits = new Apple[10]; 		
fruits[0] = new Apple();  
fruits[1] = new Jonathan(); // Jonathan是Apple的子类
try{//下面语句fruits的声明类型是Fruit[]因此编译通过,但运行时将Fruit转型为Apple错误//数组是在运行时才去判断数组元素的类型约束fruits[2] = new Fruit();//运行时抛出异常 java.lang.ArrayStoreException,这是数组协变性导致的问题
}catch(Exception e){System.out.println(e);
}

  在前一篇博客中提到过,泛型的设计就是为了防止编译通过的地方运行时出错问题的发生。如果泛型也和数组一样具备协变性,那这个问题就无法防止,所以 Java 的开发者规定泛型不具有协变性。

通配泛型

  但是,规定泛型不具备协变性,又会带来很多不方便。为了让泛型具有更好的性能, Java 开发者设计出了通配泛型。通配泛型具有三种形式:上界通配下界通配非受限通配

上界通配

  形式为<? extends T>,表示只要是T的子类即可,T定义了类型的上限(父类为上,子类为下)。
在上面WildCarNeedDemo中,只需要将max的形参列表改为(GenericStack<? extends Number> stack)就能够正常运行。因为IntegerNumber的子类,所以GenericStack<? extends Number>GenericStack<Integer>的父类。
  以上界通配符声明的泛型容器是不能添加null之外的元素的。如:

ArrayList<? extends Fruit> list = new  ArrayList<Apple>();
list.add(new Apple()); list.add(new Fruit()); //编译都报错
//可加入null
list.add(null);

  这是因为,编译器在编译时根本看不到运行时类型ArrayList<Apple>,它只认list的声明类型ArrayList<? extends Fruit>。编译器无法知道list指向的容器的元素的类型下界,自然无法判断加进来的元素是否与容器相容。所以编译器就干脆什么不让加进来。
  然而,不管list究竟指向什么类型的容器,容器的元素一定是Fruit的子类。所以可以从容器里取元素,并用Fruit类型的引用变量指向它。
  所以,上界通配的泛型容器相当于一个只读不存(注意不能存但是能删,所以是可写的)的容器。只读不写的特性,让上界通配泛型容器具有特殊的意义:作为方法参数。例如,定义一个方法handle(ArrayList<? extends Fruit> list),方法中可以对传进来的list中的元素(引用为Fruit)进行处理,但是不能添加新的元素。

非受限通配的形式为<?>,它是一种特殊的上界通配,等价于<? extends Object>。因此非受限通配的所有性质都可以参照上界通配。

下界通配

  形式为<? super T>,表示只要是T的父类即可,T定义了类型的下限。
  以下界通配符声明的泛型容器只能添加TT的子类对象。

ArrayList<? super Fruit> list = new ArrayList<Object>();
list.add(new Fruit()); 	//OK
list.add(new Apple()); 	//OK
list.add(new Jonathan()); 	//OK
list.add(new Orange());	//OK	
list.add(new Object()); //添加Fruit父类则编译器禁止,报错

  道理和上界通配是一样的,编译器只知道list指向的容器的元素的类型下界是Fruit,看不到运行时类型ArrayList<Object>。所以,编译器知道加入FruitFruit子类对象时安全的,至于Fruit的父类就无法保证了。
  从这种容器中取元素都解释为Object类,也可以强制类型转换为其他类,但是调用方法就行不通了,因为不知道取出来的对象是否有我们调用的方法。

PECS 原则

  Producer Extends,Consumer Super. 如果需要一个只读泛型类,用来Produce T,那么用 ? extends T。如果需要一个只写泛型类,用来Consume T,那么用 ? super T。如果一个泛型容器需要同时读取和写入,那么就不能用通配符。

实际上,<? extends T>也可以写(删除元素),所以说它只读是不准确的,意思是想表达不能往里面加东西。<? super T>也可以读(作为Object读出来),说它只写也是不准确的,但是想表达的意思是:从里面取出来的对象,也不知道有没有我们想要的数据成员或方法,所以一般不读。

泛型容器中元素的转移——PECS的一个应用实例

  泛型类GenericStack<E>的定义仍然沿用上文的定义。下面的代码实现了GenericStack的两个实例泛型:

GenericStack<String> strStack= new GenericStack<>();
GenericStack<Object> objStack = new GenericStack<>();		
objStack.push("Java");
objStack.push(2); //装箱
strStack.push("Sun");	

  现在我想写一个方法add,通过调用add(strStack,objStack),将strStack中的元素全部加入objStack中。可以定义下面的方法:

public static <T> void add(GenericStack<T> stack1,GenericStack<? super T> stack2){while(!stack1.isEmpty())stack2.push(stack1.pop());
}

  实际编译add(strStack,objStack)时,编译器自动推断T应该是String,并推断这条语句运行时不会出错。也可以显式地使用<String>add(strStack,objStack),但是不建议,一旦编译器推断出的实际类型和你给出的实际类型不一致,就会报错。
  当然,add的函数头还可以是:

public static <T> void add(GenericStack<? extends T> stack1,GenericStack<T> stack2);

  这时编译add(strStack,objStack),编译器推断出T应是Object

Java泛型变量推论机制浅讨论

  上面的这个实例中,都是编译器推断出T时什么类型。这是因为我们在形参列表中使用了普通泛型<T>,编译器直接根据传入对象的引用类型来推断。
  形参列表中的普通泛型给了编译器可乘之机,编译器直接通过普通泛型得到T的实际类型,然后依次检查形参列表中其他的泛型是否合法。那我如果不给编译器可乘之机呢?比如下面这样:

public static <T> void add(GenericStack<? extends T> stack1,GenericStack<? super T> stack2);

  编译器依然可以解释T,虽然这个时候编译器只能得到T的一个范围。比如,对于add(strStack,objStack)语句,编译器能得到的信息是:StringT的子类,而ObjectT的父类。显然这样的T是存在的,编译器就不会报错。那么编译器到底将T解释称什么呢?
  这种情况下,T被解释为它所能够达到的下限。下面是解释:
  栈还是上面定义的GenericStack,现在我写下面一个入口类:

public class SuperWildCarDemo {public static void main(String[] args) {GenericStack<Integer> intStack= new GenericStack<>();GenericStack<Object> objStack = new GenericStack<>();GenericStack<Object> tempStack = SuperWildCarDemo.<Number>add(intStack, objStack);}public static <T> T add(GenericStack<? extends T> stack1, GenericStack<? super T> stack2){return (T) new Object();}
}

  上述代码的第 5 5 5 行报错。报错内容如下:
在这里插入图片描述
  由于我们显示提供了TNumber,那么自然返回的stack1也被强制类型转化为Number类型了。现在我们不显式提供类型参数,看看会是怎么报错:
在这里插入图片描述
  我们没有告诉编译器T应该是什么类型,但是编译器说这个add函数返回的是Interger类型的对象。这说明编译器自行推断出TInteger
  这个例子中换成了心的泛型实例GenericStack<Integer>,是因为ObjectInteger之间还有一个中间类Number。我想说的是,在不显式提供类型实参,且编译器根据传入对象无法确定类型形参的具体类型时,编译器会把类型形参解释为它能够到达的下限。不会解释为上限,更不会解释为其他的中间类型。
  当然,如果编译器发现,根据你传入的对象推断出的T的范围是空集,那就直接报错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/831363.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何完全卸载QT

第一步&#xff0c;用QT自带的软件卸载QT 第二步&#xff0c;卸载下面路径的所有QT配置 C:用户/(你的用户)/AppData/Local/目录下所有与Qt相关内容 C:用户/(你的用户)/AppData/Local/Temp/所有与Qt相关内容 C:用户/(你的用户)/AppData/Roaming/所有与Qt相关内容

android init进程启动流程

Android系统完整的启动流程 android 系统架构图 init进程的启动流程 init进程启动服务的顺序 bool Service::Start() {// Starting a service removes it from the disabled or reset state and// immediately takes it out of the restarting state if it was in there.flags_…

vue快速入门(五十一)历史模式

注释很详细&#xff0c;直接上代码 上一篇 新增内容 历史模式配置方法 默认哈希模式&#xff0c;历史模式与哈希模式在表层的区别是是否有/#/ 其他差异暂不深究 源码 //导入所需模块 import Vue from "vue"; import VueRouter from "vue-router"; import m…

Hive 表定义主键约束

文章目录 1.建表语句2.主键约束3.主键约束的意义参考文献 1.建表语句 先看一下官方给的完整的见表语句&#xff1a; CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name -- (Note: TEMPORARY available in Hive 0.14.0 and later)[(col_name data…

全新TOF感知RGBD相机 | 高帧率+AI,探索3D感知新境界

海康机器人在近期的机器视觉新品发布会上推出的全新TOF感知RGBD相机,无疑是对当前机器视觉技术的一次革新。这款相机不仅融合了高帧率、轻松集成、体积小巧以及供电稳定等诸多优点,更重要的是,它将AI与3D感知技术完美结合,通过高帧率+AI算法,实现了对不同场景的快速捕捉与…

电脑重装系统ip地址会变吗

在数字化世界中&#xff0c;IP地址就像是每个计算机设备的“门牌号”&#xff0c;它帮助我们在互联网上进行定位和通信。然而&#xff0c;当我们的电脑系统进行重新安装时&#xff0c;许多用户会担心其IP地址是否会发生改变。虎观代理小二将带您深入探讨电脑重装系统后IP地址的…

Android Studio报错:Constant expression required

【出现的问题】&#xff1a; 使用JDK17以上版本&#xff0c;switch语句报错&#xff1a;Constant expression required 【解决方法】&#xff1a; 在gradle.properties配置文件下添加代码&#xff1a; android.nonFinalResIdsfalse 如图&#xff1a; 接着再点击右上角的Sync…

详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?

自古应用程序均从 Hello World 开始&#xff0c;你我所写的 Go 语言亦然&#xff1a; import "fmt"func main() {fmt.Println("hello world.") }这段程序的输出结果为 hello world.&#xff0c;就是这么的简单又直接。但这时候又不禁思考了起来&#xff0…

Android Studio实现简单的自定义钟表

项目目录 一、项目概述二、开发环境三、详细设计3.1、尺寸设置3.2、绘制表盘和指针3.3、动态效果 四、运行演示五、总结展望六、源码获取 一、项目概述 在安卓开发中&#xff0c;当系统自带的View已经无法满足项目需求时&#xff0c;就要自定义View。在Android中是没有与钟表有…

chrome extension插件替换网络请求中的useragent

感觉Chrome商店中的插件不能很好的实现自己想要的效果,那么就来自己动手吧。 本文以百度为例: 一般来说网页请求如下: 当前使用的useragent是User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safar…

android studio项目实战——备忘录(附源码)

成果展示&#xff1a; 1.前期准备 &#xff08;1&#xff09;在配置文件中添加权限及启动页面顺序 ①展开工程&#xff0c;打开app下方的AndroidManifest.xml,添加权限&#xff0c;如下&#xff1a; <uses-permission android:name"android.permission.CAMERA"…

【Proteus】LED呼吸灯 直流电机调速

1.LED呼吸灯 #include <REGX51.H> sbit LEDP2^0; void delay(unsigned int t) {while(t--); } void main() {unsigned char time,i;while(1){for(time0;time<100;time){for(i0;i<20;i){LED0;delay(time);LED1;delay(100-time);}}for(time100;time>0;time--){fo…

软件工程全过程性文档(软件全套文档整理)

软件项目相关全套精华资料包获取方式①&#xff1a;进主页。 获取方式②&#xff1a;本文末个人名片直接获取。 在软件开发的全过程中&#xff0c;文档是记录项目进展、决策、设计和测试结果的重要工具。以下是一个简要的软件全过程性文档梳理清单&#xff1a; 需求分析阶段…

基于 Spring Boot 博客系统开发(五)

基于 Spring Boot 博客系统开发&#xff08;五&#xff09; 本系统是简易的个人博客系统开发&#xff0c;为了更加熟练地掌握 SprIng Boot 框架及相关技术的使用。&#x1f33f;&#x1f33f;&#x1f33f; 基于 Spring Boot 博客系统开发&#xff08;四&#xff09;&#x1f…

go-mysql-transfer 同步数据到es

同步数据需要注意的事项 前提条件 1 要同步的mysql 表必须包含主键 2 mysql binlog 必须是row 模式 3 不支持程序运行过程中修改表结构 4 要赋予连接mysql 账号的权限 reload, replication super 权限 如果是root 权限则不需要 安装 go-mysql-transfer ​ git clone…

每日OJ题_DFS爆搜深搜回溯剪枝⑧_力扣980. 不同路径 III

目录 力扣980. 不同路径 III 解析代码 力扣980. 不同路径 III 980. 不同路径 III 难度 困难 在二维网格 grid 上&#xff0c;有 4 种类型的方格&#xff1a; 1 表示起始方格。且只有一个起始方格。2 表示结束方格&#xff0c;且只有一个结束方格。0 表示我们可以走过的空…

React 第十五章 Ref

React ref 是 React 中一个用于访问组件中 DOM 元素或者类实例的方式。它允许我们直接操作 DOM&#xff0c;而不需要通过 state 或 props 来更新组件。 过时 API&#xff1a;String 类型的 Refs 在最最早期的时候&#xff0c;React 中 Ref 的用法非常简单&#xff0c;类似于 …

Docker consul 的容器服务更新与发现

目录 一. consul 的相关知识 1 什么是注册与发现 2. 什么是 consul 3. zookeeper 和 consul 的区别 二. consul 部署 1. consul 服务器 2. registrator 服务器 三. consul-template 1. consul-template 的作用 2. consul-template 的具体部署运用 2.1 准备 templa…

Deep Learning Part Five RNNLM的学习和评价-24.4.30

准备好RNNLM所需要的层&#xff0c;我们现在来实现RNNLM&#xff0c;并对其进行训练&#xff0c;然后再评价一下它的结果的。 5.5.1 RNNLM的实现 这里我们将RNNLM使用的网络实现为SimpleRnnlm类&#xff0c;其层结构如下&#xff1a; 如图 5-30 所示&#xff0c;SimpleRnnlm …

设计模式: 工厂模式

工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一&#xff0c;这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 工厂模式提供了一种创建对象的方式&#xff0c;而无需指定要创建的具体类。 工厂模式属于创建型…