Java泛型设计详解

引言

在日常Java开发中,泛型是一个非常重要的特性。它提供了编译时的类型安全检查,增强了代码的可读性和可维护性。然而,对于初学者甚至一些有经验的开发者来说,泛型的使用和理解仍然是一个挑战。本文旨在深入探讨Java泛型的诞生背景、特性以及著名的PECS原则,帮助读者更好地掌握这一强大的工具。

泛型的诞生背景

为什么需要泛型

在没有泛型之前,Java中的集合类(如ArrayListHashMap等)只能存储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设计者选择了类型擦除这种方案。

类型擦除的影响

类型擦除对泛型的使用产生了一些限制和影响:

  1. 类型参数不支持基本类型:由于类型擦除会将泛型类型替换为它们的上界(通常是Object),而Object不能存储基本类型的值,因此泛型类型参数不支持基本类型。
  2. 不能实例化类型参数:由于类型擦除,无法在运行时创建泛型类型的实例。例如,无法创建T[]类型的数组。
  3. 不能创建泛型数组的实例:同样由于类型擦除,无法创建泛型数组的实例。例如,new T[10]是非法的。
  4. 运行时类型检查受限:由于泛型信息在运行时被擦除,因此无法使用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类型或其子类型(如IntegerDouble等)。

泛型方法的边界限定

同样地,我们也可以对泛型方法进行边界限定。例如,我们可以编写一个泛型方法来计算两个数值的和:

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来确保ab只能是Number类型或其子类型。然后,方法将ab转换为double类型并计算它们的和。

总结

Java泛型是一个强大的特性,它提供了编译时的类型安全检查、代码复用和灵活性。通过深入理解泛型的诞生背景、特性以及PECS原则,我们可以更好地利用泛型来编写高质量、可维护的Java代码。同时,我们也需要注意泛型的一些限制和影响,如类型擦除和边界限定等。在实际开发中,合理使用泛型将大大提高代码的质量和效率。

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

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

相关文章

C语言——实现并求出两个数的最大公约数

问题描述&#xff1a;求出两个数的最大公约数 //求两个数的最大公约数 #include<stdio.h> #include<stdlib.h> #include<string.h> #include<math.h> #include<time.h>int main() {int a,b;printf("请您输入两个数 a 和 b\n");scanf…

采用qL-MPC技术进行小型固定翼无人机的路径跟随控制

来自论文"Predictive Path-Following Control for Fixed-Wing UAVs Using the qLMPC Framework in the Presence of Wind Disturbances" 控制架构 采用的是 ULTRA-Extra无人机&#xff0c;相关参数如下&#xff1a; 这里用于guidance law的无人机运动学模型为&#…

MySQL分页

实现规则 分页原理 所谓分页显示&#xff0c;就是将数据库中的结果集&#xff0c;一段一段显示出来需要的条件。 MySQL中使用 LIMIT 实现分页 格式&#xff1a; LIMIT [位置偏移量,] 行数 总结&#xff1a;每页显示pageSize条记录&#xff0c;此时显示第pagenum页 公式…

三维无人机航迹算法的目标函数如何确定

一、定义目标函数 在三维无人机航迹算法中,目标函数的确定通常基于具体的任务需求和飞行约束。以下是一个简单的例子,展示了如何为三维无人机航迹规划定义一个目标函数。 例子:最小化飞行时间和避障的三维无人机航迹规划 1.任务描述:无人机需要从起点飞到终点,同时避开一些…

4.Python 数字类型

Python 数字类型总结 文章目录 Python 数字类型总结1. 数字类型概述特点 2. 数字类型的创建与赋值3. 数字类型转换4. 数学运算与函数math 模块cmath 模块 5. 随机数生成6. 三角函数7. 数学常量 总结 Python 提供了多种数字类型来存储和操作数值数据。这些类型包括整数、浮点数、…

《Java核心技术I》Swing用户界面组件

Swing和模型-视图-控制器设计模式 用户界面组件各个组成部分&#xff0c;如按钮&#xff0c;复选框&#xff0c;文本框或复杂的树控件&#xff0c;每个组件都有三个特征&#xff1a; 内容&#xff0c;如按钮的状态&#xff0c;文本域中的文本。外观&#xff0c;颜色&#xff0c…

【Office】Office实现shift+鼠标滚轮左右滑动

Office实现shift鼠标滚轮左右滑动 windows系统安装office之后发现&#xff0c;使用shift鼠标滚轮不能够实现左右滑动&#xff0c;我记得以前的office好像是可以的&#xff0c;然后在网上找了一下&#xff0c;找到了一个插件可以实现这个功能 OfficeScroll插件 下载地址&…

vlan和vlanif

文章目录 1、为什么会有vlan的存在2、vlan(虚拟局域网)1、vlan原理1. 为什么这样划分了2、如何实现不同交换机相同的vlan实现互访呢3、最优化的解决方法&#xff0c;vlan不同交换机4、vlan标签和vlan数据帧 5、vlan实现2、基于vlan的划分方式1、基于接口的vlan划分方式2、基于m…

Web项目图片视频加载缓慢/首屏加载白屏

Web项目图片视频加载缓慢/首屏加载白屏 文章目录 Web项目图片视频加载缓慢/首屏加载白屏一、原因二、 解决方案2.1、 图片和视频的优化2.1.1、压缩图片或视频2.1.2、 选择合适的图片或视频格式2.1.3、 使用图片或视频 CDN 加速2.1.4、Nginx中开启gzip 三、压缩工具推荐 一、原因…

02.06、回文链表

02.06、[简单] 回文链表 1、题目描述 编写一个函数&#xff0c;检查输入的链表是否是回文的。 2、解题思路&#xff1a; 快慢指针找中点&#xff1a; 利用快慢指针的技巧来找到链表的中间节点。慢指针 slow 每次移动一步&#xff0c;而快指针 fast 每次移动两步。这样&…

【CAN模块】介绍一种检查CAN模块芯片好坏的方法(SN65HVD230)

文章目录 前言一、以SN65HVD230为例介绍端口特性二、代码实现总结 前言 CAN总线收发器&#xff0c;是CAN控制器和物理总线间的接口器件&#xff0c;通常工程师会按照底层协议对其控制&#xff0c;近日笔者仔细了解了CAN总线收发器的物理原理&#xff0c;找到了一种通过观察端口…

RTMP推流平台EasyDSS在无人机推流直播安防监控中的创新应用

无人机与低空经济的关系密切&#xff0c;并且正在快速发展。2024年中国低空经济行业市场规模达到5800亿元&#xff0c;其中低空制造产业占整个低空经济产业的88%。预计未来五年复合增速将达到16.03%。 随着科技的飞速发展&#xff0c;公共安防关乎每一个市民的生命财产安全。在…

[win10] win10系统的下载及在虚拟机中详细安装过程(附有下载文件)

前言 win10 下载&#xff1a;https://pan.quark.cn/s/eb40e8ca57fb 提取码&#xff1a;VTZq 失效&#xff08;可能被官方和谐&#xff09;可评论或私信我重发 下载压缩包后解压 &#xff01;&#xff01;安装路径不要有中文 解压下载的.zip文件&#xff0c;得到.iso文件 打开…

lightRAG 论文阅读笔记

论文原文 https://arxiv.org/pdf/2410.05779v1 这里我先说一下自己的感受&#xff0c;这篇论文整体看下来&#xff0c;没有太多惊艳的地方。核心就是利用知识图谱&#xff0c;通过模型对文档抽取实体和关系。 然后基于此来构建查询。核心问题还是在解决知识之间的连接问题。 论…

[代码随想录17]二叉树之最大二叉树、合并二叉树、二搜索树中的搜索、验证二叉搜索树。

前言 二叉树的题目还是要会一流程构造函数之类的。其中还有回溯的思想 题目链接 654. 最大二叉树 - 力扣&#xff08;LeetCode&#xff09; 一、最大二叉树 思路&#xff1a;还是考察构造二叉树&#xff0c;简单来说就是给你一个数组去构建一个二叉树&#xff0c;递归来解决就…

Docker概述与基础入门

1. 什么是Docker&#xff1f; Docker 是一个开源的平台&#xff0c;用于自动化应用程序的构建、部署和管理。它允许开发人员通过将应用程序及其依赖项打包成容器镜像&#xff0c;从而确保应用可以在任何环境中一致地运行。Docker 容器是轻量级的、可移植的、且具有高度隔离性的…

C# 探险之旅:第三十六节 - 类型class之密封类Sealed Classes

嗨&#xff0c;探险家们&#xff01;欢迎再次搭乘我们的C#魔法列车&#xff0c;今天我们要去一个神秘又有点“傲娇”的地方——密封类&#xff08;Sealed Classes&#xff09;领地。系好安全带&#xff0c;咱们要深入“密封”的奇妙世界啦&#xff01; 什么是密封类&#xff1…

QTreeView 与 QTreeWidget 例子

1. 先举个例子 1班有3个学生&#xff1a;张三、李四、王五 4个学生属性&#xff1a;语文 数学 英语 性别。 语文 数学 英语使用QDoubleSpinBox* 编辑&#xff0c;范围为0到100,1位小数 性别使用QComboBox* 编辑&#xff0c;选项为&#xff1a;男、女 实现效果&#xff1a; 2…

UE5 C++ Subsystem 和 多线程

一.Subsystem先做一个简单的介绍&#xff0c;其实可以去看大钊的文章有一篇专门讲这个的。 GamePlay框架基础上的一个增强功能&#xff0c;属于GamePlay架构的范围。Subsystems是一套可以定义自动实例化和释放的类的框架。这个框架允许你从5类里选择一个来定义子类(只能在C定义…

Linux 添加spi-nor flash支持

1. spi-nor flash简介 在嵌入式ARM开发过程中通常会使用到spi-nor flash&#xff0c;主要用于固化u-boot镜像以支持spi方式启动系统。目前常用的spi-nor flash有gd25wq128e、w25q128等flash芯片&#xff0c;下述以gd25wq128e为例进行讲解。 2.调试通常遇到的问题 无法识别到…