这一文,关于 Java 泛型的点点滴滴 一(泛型基础、类型擦除)

作为一个 Java 程序员,用到泛型最多的,我估计应该就是这一行代码:

List<String> list = new ArrayList<>();

这也是所有 Java 程序员的泛型之路开始的地方啊。

不过本文讲泛型,先不从这里开始讲,而是再往前看一下,看一看没有泛型的时候,Java 代码是怎么写的,然后我们才会知道为什么要加入泛型,泛型代码该怎么写。在了解了这些内容之后,我们将继续深入介绍泛型中的 extendssuper<?> 通配符。

由于内容过长,所以分文两篇文章,本文介绍泛型的基础内容,下一篇将介绍更深入的内容:
这一文,关于Java泛型的点点滴滴 二 (extends、super、<?> 通配符、泛型与反射)

这里建议大家先完整阅读完本文再阅读下一篇文章,这样更方便理解和学习。

这里插播一下我的微信公众号,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,开始我们的正文。

为什么要设计泛型

提高代码重用性

没有泛型之前,我们写一个两数相加的函数:

public static int add(int a, int b) {return a + b;
}

看似没问题,对吧。不过这个时候我们想计算 float 类型的加法,那这个函数就不行了,因为他只能计算 int 值。此时就只能再加入一个相同的函数了:

public static float add(float a, float b) {return a + b;
}

现在我们有两个方法能够计算 int 和 float 类型的加法。那现在如果要计算 String 类型的加法呢,这两个方法就又不够用了。面对这样的需求,在没有泛型的支持下,我们只能不断地增加逻辑基本相同的方法,代码重用性极低。
这就是泛型要解决的第一个问题:提高代码重用性。
那在泛型的加持下,我们如何编写这个函数呢?

public static <T extends Number> double add(T a, T b) {return a.doubleValue() + b.doubleValue();
}

这个方法使用了泛型,它能够处理任何类型的数字相加,不需要针对每个类型编写各自的加法方法。这就大大提高了代码的重用性,有了这个方法,那些固定类型的方法就都可以删了。
特别是一些逻辑相同的代码,使用泛型不仅能够提高代码重用性,还能够提高可读性。比如说下面这段代码,真是的是非常好用:

public static <T> void printArray(T[] array) {for (T element : array) {System.out.println(element);}
}

泛型的这个特性虽然很牛了,但是这还不是 Java 要设计泛型的全部原因。因为泛型还有一个作用,那就是保证类型安全。

保证类型安全

在说泛型的这个作用之前,先问大家一个问题,咱们常用的集合 ArrayList 是 Java 哪个版本加入的呢?泛型又是 Java 哪个版本加入的呢?

答案:ArrayList 是 Java 1.2 版本加入的,而泛型是 Java 1.5 加入的。

也就是说,有一段时时间,ArrayList 不是大家普遍认识的带泛型的 ArrayList<T> 这种形式,而是一个只能存放 Object 的列表。

在那一段泛型之光没有照耀到 Java 的日子里,保证类型安全成为了 Java 程序员在使用集合时不得不考虑的事情,考虑下面这一段代码:

ArrayList list = new ArrayList();
list.add("123");
// do some work......
Integer num = (Integer) list.get(0);

这段代码没有使用泛型来使用 ArrayList,我们加入了字符串 "123",但是在使用时,我们假定程序员忘记了加入的类型,他只记得好像应该是数字,于是在获取时就直接使用了 Integer 类强转。

这样的代码是能通过编译的,但是在运行的时候,会崩溃:

 Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

这是一个典型的未使用泛型,而导致的类型安全无法保证,引发的崩溃。让程序员去保证类型安全,本身是不靠谱的做法,特别是在这种都是 Object 对象的列表中,鬼都不会知道存着的是个什么鬼。

这个时候就需要泛型出场了,泛型能够在编译时保证类型安全。例如上面的代码,我们加入泛型:

ArrayList<String> list = new ArrayList<>();
list.add("123");
Integer num = (Integer) list.get(0);

首先,ArrayList 加入泛型后,我们就知道这个列表是只能存入 String 类型的,也就不会将其转换为 Integer。那如果我非要转换呢,javac 编译器就会报错:

错误: 不兼容的类型: String无法转换为IntegerInteger num = (Integer) list.get(0);

这样类型安全就可以在编译时得到保证,不会出现在运行时的崩溃。

例子的代码很简单,大家可能看不到这一点对于软件开发有多重要,在大型复杂的项目中,这种类型安全的保证,是能减少很多运行时的崩溃的。特别是,一般像这种类型不一致的崩溃很多都是偶现的,偶现的 BUG 是最恶心的,因此使用泛型保证类型安全是十分必要的。

消除强制类型转换

泛型的这个作用其实就是上面保证类型安全这一点带来的。没有用泛型时,需要我们使用强制类型转化,但是加入泛型后,编译器已经能够知道我们存入的是什么类型,因此也就不需要我们进行强制类型转换了。

既然泛型有那么大的作用,那我们就赶紧把泛型用起来吧。

使用泛型

这一节,我们来看看如何使用系统提供的泛型类,以及其中需要注意的事项。

最常用到泛型的地方便是集合了,使用这些泛型集合类时,只需要把具体泛型参数 <T> 替换为需要的类型即可,例如 ArrayList<String>ArrayList<Number>Map<String, Integer> 等。

如果在使用泛型类时不指定类型参数,编译器会给出警告,且只能将 <T> 视为 Object 类型。这个时候就需要程序员自己去保证类型安全了,因此强烈不建议这么做,因为这样容易将类型转换异常带到运行时中去。

使用泛型基本就需要注意以上两点,下面介绍一下在使用泛型时的注意事项,这也是大家很少关注到的向上转型的问题。

在 Java 中,ArrayList<T> 是实现了 List<T> 接口,也就是说它可以向上转型为 List<T>

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable

那么问题就来了,当泛型参数不同时,还能向上转型么,说具体一点,ArrayList<String> 能转型为 List<Number> 么?

答案是不行的:

ArrayList List = new ArrayList<String>();    //Raw use of parameterized class 'ArrayList'
List<Integer> list = new ArrayList<String>();    //直接报错

为什么 Java 不允许这么转型呢?因为运行转型的话,那么对于一个 ArrayList<String> 的容器,我将其转型为 ArrayList<Integer> 就可以往里面加入 Integer 对象了,这明显会造成 ClassCastException。泛型的存在用于限定类型的,这么一搞,泛型就失去了其作用。

这里,大家可以简单理解为,当泛型参数不一样时,两个类就没有太大关系了。例如 ArrayList<Integer>List<Number> 两者完全没有继承关系。

编写泛型

知道怎么使用系统的泛型之后,我们现在就来看看如何编写自己的泛型类。

泛型作为对类型进行限制的一种方式,我们编写泛型代码,也就是对使用我们代码的人进行一种限制。在这种情况下,我们是作为其他程序员的底层,向上提供某种框架代码,让其他程序员能够在我们设定的框架中更容易地编写代码实现功能。这有点类似于库的开发者,或是框架开发者,作为这种角色,写好泛型代码就更显得尤为重要了。毕竟,你也不想让别人说,这代码写得就跟一坨屎一样吧。

编写泛型类

编写泛型类,是比普通类要复杂的。这里我们就用 Pair<F, S> 这个类作为目标,一步一步编写出一个合格的泛型类。Pair 类是 Android 开发中一个简单的使用工具类,用于存储一对相关联的对象。

我们的第一版 Pair 只能使用没有使用泛型:

public class Pair {public final String first;public final String second;
}

那这肯定是不行的,因为这个 Pair 只能存放 String 类型的 first 和 second,那了能够存放所有类型,我们就使用泛型 <T>

public class Pair<T> {public final T first;public final T second;
}

我们把 firstsecondT 来修饰,表示其这两个成员变量是 T 类型的。而这个 T 类型,Java 是不知道的,我们必须声明告诉 Java 这是一个类型,因此类名从 Pair 变成了 Pair<T>,后面的 <T> 就是我们的泛型类型声明。

上面的代码看上去没问题,但是这个 Pair<T> 只能存放的 firstsecond 必须是相同的类型 T,那不同类型的怎么办呢?这时候我们再加一个泛型不就行了:

public class Pair<F, S> {public final F first;public final S second;
}

在加入两个泛型之后,firstsecond 的类型对应不同的泛型,这样就可以表示不同的类型了,注意 FS 这两个不同的泛型都需要在类上进行声明。

我们在为 Pair<F, S> 添加个构造方法:

public class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = first;this.second = second;}
}

这算是一个简单的泛型类,那接下来,我们再为它编写一个泛型方法。

编写泛型方法

此处的泛型方法是指静态方法,而不是成员方法。这两种方法在使用泛型时是有一些区别的,其中最重要的一点就是,静态方法是不能使用类上声明的泛型类型,必须得自己声明泛型类型。例如,下面的代码将编译错误:

public static class Pair<F, S> {public final F first;public final S second;//编译错误,F、S 类型不能在 static 方法上使用public static Pair<F, S> create(F a, S b) {return new Pair<F, S>(a, b);}
}

可以想一想,为什么静态方法不能使用类上已经声明的泛型类型呢?

在回答这个问题之前,我们可以先想一下,类上的泛型类型,是在什么时候确定下来的呢?是在类创建的时候,我们在 new 的时候是需要提供具体类型的,这个时候泛型就被具体化为某个特定类型。不同的对象可能被创建为不同的类型,而静态方法只跟类相关,跟具体对象无关,而这些泛型又是跟具体对象相关的。所以静态对象不能使用类上声明的泛型也就变得合理了。

那要想使静态方法使用泛型,那就必须这个静态方法自己声明泛型:

public static <F, S> Pair <F, S> create(F a, S b) {return new Pair<F, S>(a, b);
}

这个静态方法在函数名前使用 <F, S> 来声明了两个泛型,那么后续这两个泛型就可以在这个函数中使用了。此时注意,这里的 FS 虽然与 Pair 上的 FS 泛型看似相同,实际上是没有任何关系的。所以为了避免产生误会,一般都会使用不同的泛型名,例如将这个方法的 <F, S> 变成 <A, B>

public static class Pair<F, S> {public final F first;public final S second;public static <A, B> Pair <A, B> create(A a, B b) {return new Pair<A, B>(a, b);}
}

这样才能够清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

在使用时,我们可以使用如下代码创建一个 Pair<F, S> 实例:

Pair<String, Integer> pair = Pair.create("123", 123);

这里总结一下编写泛型需要注意的几点:

  • 编写泛型时,需要定义泛型类型 <T>
  • 静态方法不能引用类上的泛型类型 <T>,必须定义自己方法特有的泛型类型;
  • 泛型可以同时定义多个,例如 <F, S><F、S、T>

在这里我们需要注意泛型的一个限制,那就是不能使用泛型类型直接创建对象。这一点也好理解,T 是什么类型只有在使用时,指定了泛型的具体类型才能确定。T 类型是一个抽象的类型,它是无法直接 new 出来的,就像你无法直接 new 一个 interface 一样。例如下面的代码是错误的:

public static class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = new F();        //错误this.second = new S();       //错误}
}

这里使用 F 类型的默认构造,设想一下假如这个类型被确定为一个没有默认构造方法的类型呢。所以使用泛型类型创建对象是不行的。

Java 的泛型实现方式:类型擦除

上面的几节介绍了泛型的好处,泛型的使用,那这一节我们就来看看 Java 是如何实现泛型技术的。

首先,泛型编程并非 Java 特有的,在其他语言 C++、C# 上都有类似的技术,只不过名称不同而已,例如 C++ 上叫模版。在这些技术的加持下,程序员可以编写与具体类型无关的代码,只需要在使用时指定具体类型,从而提高代码的复用性;并且在编译时进行类型检查,减少运行时错误。

Java 的泛型是通过类型擦除(Type Erasure)来实现的。也就是说在编译时将泛型类型擦除,替换为其上限类型(通常为 Object),并在必要时插入类型转换。这种机制在编译时处理泛型类型,而在运行时移除了所有的泛型信息,因此叫做类型擦除。

这也就意味着,Java 的泛型是由编译器实现的,在编译成 class 文件时类型信息已经被擦除了,因此运行时,Java 虚拟机是没有任何泛型信息的。

例如上面我们编写的 Pair 的这个类,在我们看来,它是这样的,在源代码阶段,里面是包含泛型信息的:

public static class Pair<F, S> {public final F first;public final S second;public Pair(F first, S second) {this.first = first;this.second = second;}public static <A, B> Pair<A, B> create(A a, B b) {return new Pair<A, B>(a, b);}
}

那么在虚拟机的视角,它是这样的:

public class Pair {private Object first;private Object last;public Pair(Object first, Object last) {this.first = first;this.last = last;}
}

从这里就能看到,这个 Pair 在运行时已经没有泛型信息了,所有的泛型类型都被替换为了 Object

那么既然我们定义的泛型类型最终都变成了 Object,那我们就知道了 Java 泛型的一个局限:泛型类型 <T> 不能是基本类型。
因为像 intfloat 这些基本类型不是 Object 的子类,所以我们必须使用包装类:

Pair<float, int> pair = Pair.create(3.15, 123);    //编译错误
Pair<Float, Integer> pair = Pair.create(3.15F, 123);    //编译通过

尽管 Java 的泛型在编译时通过类型擦除机制移除了泛型类型信息,但 Java 编译器会在 class 文件中保留一些泛型信息,以便工具和开发人员能够利用这些信息进行反射和调试。所以如果大家把这个类编译为 class 文件之后,再查看它的反编译的内容,会发现它是有一些泛型信息的。但这并不意味着 JVM 在运行时会携带这些类型信息,既然是类型擦除,也就是说泛型类型参数被擦除并替换为其边界类型,如果没有指定边界,则默认为 Object

这里又引入了边界类型这个概念,在下一篇文章中,我们就来详细聊聊这个边界类型,这也是泛型中比较重要和难的点。

好了,在学习了 Java 泛型的基础内容,下面就接着学习泛型中更深入的内容吧:
这一文,关于Java泛型的点点滴滴 二 (extends、super、<?> 通配符、泛型与反射)

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

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

相关文章

富唯智能转运机器人:高效、智能、未来的选择

在现代工业中&#xff0c;高效的物流和物料处理是提升生产效率的关键。富唯智能转运机器人&#xff0c;以其卓越的技术和智能化的设计&#xff0c;为各行业提供了完美的解决方案。 产品概述 富唯智能转运机器人搭载ICD系列核心控制器&#xff0c;拥有多种移载平台&#xff0c…

dpdk编译安装以及接收udp报文(基于ubuntu)

目录 1、编译 2、设置运行环境 3、使用dpdk接收udp报文 3.1、设置发送端arp信息 3.2、测试 3.3、代码 4、其他 1、编译 代码下载&#xff1a; DPDK 下载版本&#xff1a;DPDK 19.08.2 export RTE_SDK/root/dpdk-stable-19.08.2/ export RTE_TARGETx86_64-native-li…

Python 教程(五):理解条件语句和循环结构

目录 专栏列表前言条件语句if 语句elif 语句else 语句示例 循环结构for 循环while 循环break 和 continue实例演示 循环控制语句range 函数enumerate 函数 模式匹配总结 在前四篇教程中&#xff0c;我们学习了 Python 的基本语法和数据结构。本篇教程&#xff0c;我们将深入探讨…

lua 游戏架构 之 游戏 AI (八)ai_tbl 行为和优先级

定义一系列的AI行为类型和它们的优先级&#xff0c;以及一个映射表ai_tbl来关联每种AI行为类型与对应的脚本文件和优先级。以下是对代码的详细解释&#xff1a; lua 游戏架构 之 游戏 AI &#xff08;一&#xff09;ai_base-CSDN博客https://blog.csdn.net/heyuchang666/artic…

【ACM独立出版,高录用】第四届物联网与机器学习国际学术会议(IoTML 2024,8月23-25)

2024年第四届物联网与机器学习国际学术会议&#xff08;IoTML 2024&#xff09;将于2024年8月23-25日在中国南昌召开。 会议将围绕着物联网和机器学习开展&#xff0c;探讨本领域发展所面临的关键性挑战问题和研究方向&#xff0c;以期推动该领域理论、技术在高校和企业的发展和…

【全国大学生电子设计竞赛】2022年A题

&#x1f970;&#x1f970;全国大学生电子设计大赛学习资料专栏已开启&#xff0c;限时免费&#xff0c;速速收藏~

信息收集总结

关注内容 1. 企业股权架构&#xff1a;了解公司的股权结构&#xff0c;确定控股子公司&#xff0c;收集边缘业务系统资产。 2. 网站备案&#xff1a;获取公司的备案信息&#xff0c;包括根域名及其子域名。 3. 对外发布的产品&#xff1a;收集公司公开发布的产品信息&…

剑指Meta?1230 亿参数,Mistral 发布 Large 2 旗舰 AI 模型

7 月 25 日消息&#xff0c;AI 竞赛日益激烈&#xff0c;Meta 公司昨日推出开源 Llama 3.1 模型之后&#xff0c;法国人工智能初创公司 Mistral 也加入了竞争行列&#xff0c;在今日凌晨推出了新一代旗舰模型 Mistral Large 2。 Mistral AI 发布 Mistral Large 2&#xff0c;1…

[算法题]添加字符

题目链接: 添加字符 因为数据量很小, 所以直接两层遍历暴力求解, 依次将字符串 A 与字符串 B 相同位进行比对, 每轮记录当前轮次不相等的位数是几位, 并与历史上记录的比较取小值, 图示: 经过比较后, 题解就为 1, 空白处不用管, 因为题意是可以在开头和末尾添加字符的, 添加与…

c生万物系列(加减乘除模篇)

为了提高c语言的运行效率&#xff0c;我们需要采用更高效的运算&#xff0c;那么切入点就是随处可见的基本运算符合&#xff0c;从底层架构考虑&#xff0c;加减乘除的效率比位运算低很多&#xff0c;为了能够更好迎合CPU的二进制&#xff0c;有必要取代基本的加减乘除以及求余…

【“微软蓝屏”事件暴露了网络安全哪些问题?】建设安全稳固的网络基础设施

目录 前言一、软件更新流程中的风险管理和质量控制机制&#xff08;一&#xff09;测试流程及风险识别&#xff08;二&#xff09;风险管理策略&#xff08;三&#xff09;质量控制措施 二、预防类似大规模故障的最佳方案或应急响应对策&#xff08;一&#xff09;冗余系统设计…

2024最火的知识付费系统小程序+PC+H5三端数据互通支持采集资源开源版

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 系统含带 裂变模式 可以助力好友来获取资源共享 分站功能 独立后台 会员功能 卡密功能 二级分销功能等 自行研究看 后期有更新新版会在持续发布 目前版本是3.5 是我花三天时间修复的 …

科研论文之Word论文编辑

这篇文章介绍在word中怎么编辑论文&#xff0c;包括论文的模板、论文的字体设置、论文的插图、论文的参考文献等等。 为便利知识传播&#xff0c;我的所有文章都不会设置收费专栏。但文章写作不易&#xff0c;如有可能麻烦打赏一下&#xff0c;金额随意。收款码见下图&#xff…

《程序猿入职必会(5) · CURD 页面细节规范 》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

nodejs - MongoDB 学习笔记

一、简介 1、MongoDB 是什么 MongoDB 是一个基于分布式文件存储的数据库&#xff0c;官方地址 https://www.mongodb.com/ 2、数据看是什么 数据库&#xff08;DataBase&#xff09;是按照数据结构来组织、存储和管理数据的应用程序。 3、数据库的作用 主要作用是 管理数据…

RedHat9 | Ansible 编写循环和条件任务

环境版本说明 RedHat9 [Red Hat Enterprise Linux release 9.0]Ansible [core 2.13.3]Python [3.9.10]jinja [3.1.2] 1. 利用循环迭代任务 通过利用循环&#xff0c;管理员无需编写多个使用同一模块的任务。Ansible支持使用loop关键字对一组项目迭代任务&#xff0c;通过配置…

音视频入门基础:WAV专题(3)——FFmpeg源码中,判断某文件是否为WAV音频文件的实现

一、引言 通过FFmpeg命令&#xff1a; ./ffmpeg -i XXX.wav 可以判断出某个文件是否为WAV格式的音频文件&#xff1a; 所以FFmpeg是怎样判断出某个文件是否为WAV格式的音频文件呢&#xff1f;它内部其实是通过wav_probe函数来判断的。从文章《FFmpeg源码&#xff1a;av_prob…

Spring Boot集成OpenPDF和Freemarker实现PDF导出功能并附水印

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

关于@JsonSerialize序列化与@JsonDeserialize反序列化注解的使用(密码加密与解密举例)

注&#xff1a;另一种方式参考 关于TableField中TypeHandler属性&#xff0c;自定义的类型处理器的使用&#xff08;密码加密与解密举例&#xff09;http://t.csdnimg.cn/NZy4G 1.简介 1.1 序列化与反序列化 学习注解之前&#xff0c;我们可以先了解一下什么是序列化与反序列…

遵义网站建设安全性保证

随着互联网的发展和普及&#xff0c;网站的建设成为了一个重要的工作&#xff0c;也是企业宣传的重要渠道。然而&#xff0c;随之而来的安全问题也是不容忽视的。为了保证遵义网站建设的安全性&#xff0c;我们需要采取一系列的措施。 首先&#xff0c;要选择合适的服务器和主机…