Java泛型设计详解
引言
在日常Java开发中,泛型是一个非常重要的特性。它提供了编译时的类型安全检查,增强了代码的可读性和可维护性。然而,对于初学者甚至一些有经验的开发者来说,泛型的使用和理解仍然是一个挑战。本文旨在深入探讨Java泛型的诞生背景、特性以及著名的PECS原则,帮助读者更好地掌握这一强大的工具。
泛型的诞生背景
为什么需要泛型
在没有泛型之前,Java中的集合类(如ArrayList
、HashMap
等)只能存储Object
类型的对象。这意味着在添加和获取元素时,需要进行显式的类型转换。这种做法不仅繁琐,而且容易导致ClassCastException
。例如:
ArrayList list = new ArrayList();
list.add(11);
list.add("ssss");
for (int i = 0; i < list.size(); i++) {System.out.println((String) list.get(i));
}
上述代码在运行时会抛出ClassCastException
,因为列表中的元素既有Integer
类型,又有String
类型。在尝试将Integer
类型转换为String
类型时,会抛出异常。
为了解决这一问题,Java 5引入了泛型。泛型允许在编译时指定集合中元素的类型,从而在编译阶段就能检查类型错误,避免运行时异常。例如:
List<String> list = new ArrayList<>();
list.add("hahah");
list.add("ssss");
for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));
}
在上面的代码中,我们只能向列表中添加String
类型的元素,否则编译器会报错。这样,在调用get()
方法时,无需进行显式的类型转换,因为编译器已经确保了元素的类型。
泛型的思想来源
泛型的思想并不是Java独有的,它在其他编程语言中早已存在。例如,C++中的模板(Templates)就是一种参数化类型的机制。模板允许开发者编写与类型无关的代码,在编译时根据实际的类型参数生成具体的代码。Java的泛型在很大程度上借鉴了C++模板的思想。
泛型的特性
类型安全
泛型最显著的特性之一是类型安全。通过泛型,可以在编译时捕获类型错误,而不是在运行时。这大大提高了程序的稳定性和可靠性。例如:
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
int firstInt = intList.get(0); // 正确,无需类型转换
// intList.add("three"); // 编译错误,无法添加String类型元素
在上面的代码中,尝试向intList
中添加String
类型的元素会导致编译错误。这是因为编译器在编译时已经检查了元素的类型,并确保了类型的一致性。
代码复用
泛型允许编写适用于多种数据类型的代码,从而避免了代码重复。例如,可以编写一个通用的排序方法,该方法可以对任何实现了Comparable
接口的对象进行排序:
java复制代码 public static <T extends Comparable<T>> void sort(List<T> list) { // 实现排序算法 }
在这个方法中,T
是一个类型参数,它表示列表中的元素类型。由于T
继承自Comparable<T>
,因此可以对列表中的元素进行比较和排序。这样,一个排序方法就可以适用于任何类型的列表,而无需为每种类型编写单独的排序方法。
灵活性
泛型提供了在运行时动态指定类型的能力,从而增加了代码的灵活性。例如,可以编写一个泛型类,该类可以在创建对象时指定类型参数:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;}
public T get() {
return t;}
}
// 使用Box类
Box<Integer> box1 = new Box<>();
box1.set(12);
Integer value1 = box1.get();
Box<String> box2 = new Box<>();
box2.set("abc");
String value2 = box2.get();
在这个例子中,Box
类是一个泛型类,它使用类型参数T
来表示存储的元素类型。通过创建Box<Integer>
和Box<String>
对象,可以分别存储整数和字符串类型的元素。
PECS原则的由来
什么是PECS原则
PECS原则是“Producer Extends, Consumer Super”的缩写,它是由Joshua Bloch在《Effective Java》一书中提出的。PECS原则旨在指导开发者在使用Java泛型时的通配符选择,特别是在涉及到集合类时。
- Producer Extends:当你需要从一个数据结构获取(生产)元素时,应该使用
extends
通配符。这样,你可以从该数据结构读取到的类型为该通配符或其任何子类型的对象。 - Consumer Super:当你需要向一个数据结构写入(消费)元素时,应该使用
super
通配符。这允许你写入指定的类型或其任何超类型(父类型)的对象到该数据结构中。
PECS原则的由来
PECS原则的提出是为了解决在使用泛型通配符时可能出现的类型安全问题。在没有PECS原则指导的情况下,开发者可能会错误地选择通配符,从而导致类型不匹配或类型转换异常。
例如,考虑以下两个方法:
public void printElements(List<? extends Number> list) {
for (Number number : list) {System.out.println(number);}
}
public void addElements(List<? super Integer> list) {list.add(1);list.add(2);
}
在printElements
方法中,我们使用了extends
通配符。这是因为我们只需要从列表中读取元素,而不需要向列表中添加元素。使用extends
通配符可以确保我们读取到的元素是Number
类型或其子类型,从而避免了类型转换异常。
在addElements
方法中,我们使用了super
通配符。这是因为我们需要向列表中添加元素,而添加的元素类型是Integer
。使用super
通配符可以确保我们可以将Integer
类型或其父类型的对象添加到列表中。
PECS原则的应用实例
使用extends
进行读取操作
假设我们有一个Animal
类和一个Bird
类,其中Bird
继承自Animal
。现在,我们想要编写一个方法来打印动物列表中所有鸟的名字:
public class Animal {String name;
public Animal(String name) {
this.name = name;}
public String getName() {
return name;}
}
public class Bird extends Animal {
public Bird(String name) {
super(name);}
}
public void printBirdNames(List<? extends Bird> birdList) {
for (Bird bird : birdList) {System.out.println(bird.getName());}
}
// 使用方法
List<Bird> birdList = Arrays.asList(new Bird("Parrot"), new Bird("Sparrow"));
printBirdNames(birdList);
在这个例子中,printBirdNames
方法接受一个List<? extends Bird>
类型的参数。这意味着我们可以传递一个包含Bird
类型或其子类型的列表给该方法。由于我们只从列表中读取元素,因此使用extends
通配符是合适的。
使用super
进行写入操作
现在,假设我们想要编写一个方法来向动物列表中添加一些默认的鸟:
public void addDefaultBirds(List<? super Bird> animalList) {animalList.add(new Bird("Default Parrot"));animalList.add(new Bird("Default Sparrow"));
}
// 使用方法
List<Animal> animalList = new ArrayList<>();
addDefaultBirds(animalList);
在这个例子中,addDefaultBirds
方法接受一个List<? super Bird>
类型的参数。这意味着我们可以传递一个List<Animal>
、List<Bird>
或List<Object>
类型的列表给该方法。由于我们需要向列表中添加元素,并且添加的元素类型是Bird
,因此使用super
通配符是合适的。
泛型的类型擦除
什么是类型擦除
Java的泛型是在编译时实现的,而不是在运行时。这意味着在编译后生成的字节码中,泛型信息会被擦除。这种机制被称为类型擦除(Type Erasure)。
类型擦除的目的
类型擦除的目的是为了保持与Java 5之前版本的兼容性。由于泛型是在Java 5中引入的,而在此之前已经存在大量的Java代码,因此为了兼容这些代码,Java设计者选择了类型擦除这种方案。
类型擦除的影响
类型擦除对泛型的使用产生了一些限制和影响:
- 类型参数不支持基本类型:由于类型擦除会将泛型类型替换为它们的上界(通常是
Object
),而Object
不能存储基本类型的值,因此泛型类型参数不支持基本类型。 - 不能实例化类型参数:由于类型擦除,无法在运行时创建泛型类型的实例。例如,无法创建
T[]
类型的数组。 - 不能创建泛型数组的实例:同样由于类型擦除,无法创建泛型数组的实例。例如,
new T[10]
是非法的。 - 运行时类型检查受限:由于泛型信息在运行时被擦除,因此无法使用
instanceof
关键字来检查泛型类型的实例。例如,if (obj instanceof T)
是非法的。
类型擦除的实现原理
在编译时,Java编译器会对泛型代码进行类型擦除处理。具体来说,编译器会将泛型类型替换为它们的上界,并插入必要的类型转换代码以确保类型安全性。
例如,考虑以下泛型方法:
public static <T> T getFirstElement(List<T> list) {
return list.get(0);
}
在编译后,该方法会被转换为类似以下的代码:
public static Object getFirstElement(List list) {
return (T) list.get(0); // 插入类型转换
}
注意,在转换后的代码中,泛型类型T
被替换为了Object
,并且在返回语句中插入了类型转换。这是为了确保类型安全性,因为编译器知道在调用getFirstElement
方法时,传入的列表应该是特定类型的列表(例如List<String>
),因此可以将返回的对象强制转换为该类型。
泛型的边界限定
什么是边界限定
边界限定(Bounded Types)允许对泛型类型参数施加约束。通过边界限定,可以确保泛型类型参数是某个特定类或接口的子类型或实现类型。
边界限定的语法
边界限定的语法如下:
java复制代码 <T extends Bound>
其中,T
是泛型类型参数,Bound
是对T
的约束。Bound
可以是一个类或接口。
边界限定的应用实例
泛型类的边界限定
假设我们想要编写一个泛型类,该类只能处理数值类型的对象。我们可以使用边界限定来确保这一点:
public class NumericBox<T extends Number> {
private T value;
public void setValue(T value) {
this.value = value;}
public T getValue() {
return value;}
}
// 使用NumericBox类
NumericBox<Integer> intBox = new NumericBox<>();
intBox.setValue(123);
Integer intValue = intBox.getValue();
NumericBox<Double> doubleBox = new NumericBox<>();
doubleBox.setValue(123.45);
Double doubleValue = doubleBox.getValue();
在这个例子中,NumericBox
类是一个泛型类,它使用边界限定T extends Number
来确保T
只能是Number
类型或其子类型(如Integer
、Double
等)。
泛型方法的边界限定
同样地,我们也可以对泛型方法进行边界限定。例如,我们可以编写一个泛型方法来计算两个数值的和:
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
// 使用sum方法
double result1 = sum(123, 456);
double result2 = sum(123.45, 67.89);
在这个例子中,sum
方法是一个泛型方法,它使用边界限定T extends Number
来确保a
和b
只能是Number
类型或其子类型。然后,方法将a
和b
转换为double
类型并计算它们的和。
总结
Java泛型是一个强大的特性,它提供了编译时的类型安全检查、代码复用和灵活性。通过深入理解泛型的诞生背景、特性以及PECS原则,我们可以更好地利用泛型来编写高质量、可维护的Java代码。同时,我们也需要注意泛型的一些限制和影响,如类型擦除和边界限定等。在实际开发中,合理使用泛型将大大提高代码的质量和效率。
相关文章:
Java泛型设计详解
引言 在日常Java开发中,泛型是一个非常重要的特性。它提供了编译时的类型安全检查,增强了代码的可读性和可维护性。然而,对于初学者甚至一些有经验的开发者来说,泛型的使用和理解仍然是一个挑战。本文旨在深入探讨Java泛型的诞生…...

用ue5打开网址链接
需要用到 Launch URL 这个函数 字面意思就是打开填写的链接网页 这里填写的是百度,按下Tab键后就会打开百度的网页...
【大数据】-- 读放大和写放大
目录 一、定义 1. 读放大(Read Amplification) 定义 原因 优化方法 2. 写放大(Write Amplification) 定义 原因 优化方法 对比与联系 二、举例 1. Hadoop(HDFS) 读放大 写放大 2. Flink 读放大 写放大 3. Hive 读放大 写放大 4. Presto 读放大 写放…...

【前端】JavaScript 抽取字符串特定部分题目详解与实现思路
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 💯前言💯题目描述💯核心步骤与实现解析1. 分割字符串为数组(split 方法)2. 使用 filter 提取名字(偶数索引判断)3. 使…...

CNCF云原生生态版图-分类指南(一)- 观测和分析
CNCF云原生生态版图-分类指南(一)- 观测和分析 CNCF云原生生态版图-分类指南一、观测和分析(Observability and Analysis)(一)可观测性(Observablility)1. 是什么?2. 解决…...

热更新解决方案3 —— xLua
概述 xLua框架导入和AB包相关准备 xLua导入 其它的导入 C#调用Lua 1.Lua解析器 using System.Collections; using System.Collections.Generic; using UnityEngine; //引用命名空间 using XLua;public class Lesson1_LuaEnv : MonoBehaviour {// Start is called before the fi…...
如何让ai在游戏中更像一个人?
开题开了一整年是我没想到的,还因此延毕了……我重新梳理一下我想做的研究以及相关痕迹。 我2023年3月找到的导师。起初我发现了在玩RTS游戏中会出现很多固定的套路,选手为此要做大量的练习,我就在想如何把这部分内容借助状态机这种流程给…...

websocket_asyncio
WebSocket 和 asyncio 指南 简介 本指南涵盖了使用 Python 中的 websockets 库进行 WebSocket 编程的基础知识,以及 asyncio 在异步非阻塞 I/O 中的作用。它提供了构建高效 WebSocket 服务端和客户端的知识,以及 asyncio 的特性和优势。 1. 什么是 WebS…...

如何在NGINX中实现基于IP的访问控制(IP黑白名单)?
大家好,我是锋哥。今天分享关于【如何在NGINX中实现基于IP的访问控制(IP黑白名单)?】面试题。希望对大家有帮助; 如何在NGINX中实现基于IP的访问控制(IP黑白名单)? 1000道 互联网大…...

Y3编辑器文档4:触发器1(界面及使用简介、变量作用域、入门案例)
文章目录 一、触发器简介1.1 触发器界面1.2 ECA语句编辑及快捷键1.3 参数设置1.4 变量设置1.5 实体触发器1.6 触发器复用 二、触发器的多层结构2.1 子触发器(在游戏内对新的事件进行注册)2.2 触发器变量作用域 三、入门案例3.1 使用触发器实现瞬间移动3.…...

echarts图表自定义配置(二)——代码封装
下图是初版,火山图的代码。可以看出,里面的变量,逻辑,函数存在冗余,基本上都是改了参数,同样的get和set,去刷新图表;对于往后继续开发十几二十个图表,会很麻烦。因此需要…...

02、10个富士胶片模拟的设置
二色彩 1、色彩的加减控制全局的饱和度增减; 2、色彩效果只提升暖色系饱和度; 3、FX蓝色大幅度提升蓝色系饱和度; 4、三个参数都不改变颜色的色相。 2.1 色彩 色彩调整的是拍摄画面整体的色彩饱和程度 2.2色彩效果 调整的是画面中暖色…...

鸿蒙系统-前端0帧起手
鸿蒙系统-前端0帧起手 先search 一番 找到对应的入门文档1. 运行项目遇到问题 如下 (手动设计npm 的 registry 运行 npm config set registry https://registry.npmjs.org/)2.运行后不支持一些模拟器 配置一下(如下图,运行成功&am…...

211-基于FMC的1路1.5G ADC 1路 2.5G DAC子卡
一、板卡概述 FMC-1AD-1DA-1SYNC是我司自主研发的一款1路1G AD采集、1路2.5G DA回放的FMC、1路AD同步信号子卡。板卡采用标准FMC子卡架构,可方便地与其他FMC板卡实现高速互联,可广泛用于高频模拟信号采集等领域。 二、功能介绍 2.1 原理框图 2.2 硬件…...

获取微信用户openid
附上开发文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 开发之前,准备事项 一个已认证过的服务号|基本信息配置js域名和网站授权域名配置最后确认当前账号网页授权功能是否开通,没有开通的无法获取到用户授权开发人…...
MultiRECloudSim使用
MultiRECloudSim使用 简介 MultiRECloudSim是一个用于云计算环境下的模拟器相关工具,它主要用于模拟和评估云计算中的资源分配、任务调度等多种场景。它可能是基于CloudSim这个基础的云计算模拟器进行扩展而来,CloudSim提供了基本的云计算模拟功能,如数据中心、虚拟机、任务…...

智能设备安全-固件逆向分析
固件逆向分析实验报告-20241022 使用固件常用逆向分析工具,对提供的固件进行文件系统提取,并记录逆向分析实验过程,提交实验报告(报告要求图文并茂,对涉及到的关键步骤附截图说明)。具体任务如下࿱…...

【小白包会的】使用supervisor 管理docker内多进程
使用supervisor 管理docker内多进程 一般情况下,一个docker是仅仅运行一个服务的 但是有的情况中,希望一个docker中运行多个进程,运行多个服务,也就是一个docker容器执行多个服务。 调研了一下,发现可以通过**super…...

使用navicat新旧版本,连接PostgreSQL高版本报错问题图文解决办法
使用navicat新旧版本,连接PostgreSQL高版本报错问题图文解决办法 一、问题现象:二、出现原因三、解决方法:1、升级Navicat版本:2、使用低版本的postgreSQL:3、修改Navicat的dll二进制文件:navicat版本15nav…...

IDEA 未启用lombok插件的Bug
项目中maven已引用了lombok依赖,之前运行没有问题的,但有时启动会提示: java: You arent using a compiler supported by lombok, so lombok will not work and has been disabled. Your processor is: com.sun.proxy.$Proxy8 Lombok support…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...

算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...