当前位置: 首页 > ds >正文

数据结构准备:包装类+泛型

提示:欢迎评论区讨论!

数据结构准备:包装类+泛型

    • 1. 包装类
      • 1.1 为什么要学包装类?
      • 1.2 装箱和拆箱
        • 1.2.1 注意事项
    • 2. 泛型
      • 2.1 写法
      • 2.2 实现机制---类型擦除
      • 2.3 什么是原始类型?


1. 包装类

*包装类(Wrapper Classes)*是Java为8种基本数据类型提供的引用类型,各自对应关系为:

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

记忆技巧:

可以清晰看到,只有int和char的包装类与基本类型相比变化较大,其余的包装类均为各自基本类型的首字母大写.

1.1 为什么要学包装类?

  1. 理解Java一切皆对象理念,包装类把基本类型转化为对象,保证了统一的对象模型.

  2. 让基础类型能参与面向对象编程,基本类型如int,double等本身没有属性和方法,不能调用成员函数,而包装类(如Integer,Double)提供了丰富的方法:

// 错误!ArrayList不能存储基本类型int// ArrayList<int> list = new ArrayList<>(); // 正确!必须使用包装类IntegerArrayList<Integer> list = new ArrayList<>();list.add(10); // 10会自动装箱为Integer对象
  1. 提供了许多有用的静态方法和常量

在这里插入图片描述

  1. 包装类让基本类型也能用泛型,泛型不支持基本类型,有了包装类,就能把基本类型装箱成对象,用在泛型中(后续讲解),学习包装类为后面学习集合,泛型打下基础.

  2. 为理解自动装箱与拆箱机制打下基础

1.2 装箱和拆箱

上小节在列举包装类意义时提到"学习包装类有助于理解自动装箱和拆箱机制",现在来讲解一下何为装箱拆箱.

  • 装箱(Boxing):将基本类型转->(转换为)包装类对象
  • 拆箱(Unboxing):将包装类对象->(转换为)对应的基本数据类型

在Java5之前,这个过程必须由程序员手动完成,即手动拆箱和手动装箱:

在这里插入图片描述

可以看到,代码较为繁琐,若是操作集合,会充满各种.valueOf()和.xxxValue()调用

Java5之后,编译器在编译阶段为我们插入了必要的代码,让我们可以直接赋值,就像类型自动转换一样:

在这里插入图片描述

编译器会自动补全Integer.valueOf()和.intValue()方法,使代码看起来简洁了很多~

但是要知道,自动和手动在运行时性能并无差异:

在这里插入图片描述

自动拆箱/装箱只是语法糖,即这种语法对功能没有影响,但是更方便程序员使用,让代码更简洁,易读.而最终生成的字节码指令完全一致,运行时性能完全一样

1.2.1 注意事项

装箱拆箱很方便,但是也隐含了一些潜在问题,若不理解其本质的话,容易犯错:

在这里插入图片描述

当我们运行上述代码时结果是一个true一个false,值为100的那个是true,200的那个是false,我们知道"=="用于引用类型时,比的是对象的地址(是否是同一个对象)而不单单是数值,两次比较的对象数值都相等,也就是说,a和b是同一个对象,c和d则是不同的对象.

这是为什么呢?

只有源码能告诉我们答案了,我们刚才已经分析,自动和手动装箱的底层都是valueOf方法,因此答案可能就出在valueOf上.每个包装类都实现了该方法,我们现在研究的是Integer的valueOf方法,于是我们可以按照下列步骤找到该方法:

双击shift键,在搜索框中输入Integer,并勾选右上方"include non-project items"

在这里插入图片描述

单击打开匹配选项,此时就打开了包装类Integer的源码

然后,点击idea左侧菜单栏的structure图标,在弹出窗口中找到valueOf(int)方法即可

在这里插入图片描述

现在我们来仔细看一下Integer.valueOf(int)方法的逻辑:

在这里插入图片描述

不用在意变量名,看结构就可以看到逻辑大概是:valueOf方法会对传入的int数值i进行一个比较,当i位于[IntegerCache.low,IntegerCache.high]区间时将对i进行一些处理后,然后从一个名为IntegerCache.cache的数组中查找并返回一个什么东西;而若i不在这个区间,将new一个新对象

结合一开始给出的运行结果,可以判断出,值为100时走的是上面的return;值为200时则是下面的return.(执行下面的return时,每次都会新new一个对象,地址自然不一样)

那么第一个return到底是什么逻辑?点开IntegerCache的源码,

在这里插入图片描述

可以看到,该类中定义了两个常量,low为-128,high为127,200不在[-128,127]这个区间内,因此每次都会新创建一个对象,导致c和d即使值相同,但是两个完全不同的对象.也验证了我们的想法.

此外,我们还可以看到该类下有一个名为cache,类型为的Integer[]的数组,在valueOf方法的主体中,有该数组的处理逻辑:IntegerCache.cache[i+(-IntegerCache.low)],这是一个巧妙的数组索引运算:

  • 数组下标从0开始,此时满足0=i+(-(-128)),算出i=-128,这就是下标为0时对应数组元素中存储的值
  • 根据区间[-128,127],可知数组的长度为256,数组下标为0->255,最后一个下标255对应的i值为255-128=127

可知数组cache中存储的值为-128->127,正正好就是对输入的i进行比较的那个区间

在这里插入图片描述

当i为100时,可知其对应的数组下标为100+128=228,在下标区间[0,255]内,于是程序就知道了:cache数组中有这个Integer对象,直接返回就好,不需要再创建了,这也就是当a,b都取100时,程序判定两者是同一个对象的原因.

这样做的用意----性能优化-享元模式
IntegerCache是Integer类的一个私有内部类,核心工作就是预先创建好缓存范围内的所有Integer对象,通过缓存和重用对象,避免了大量重复对象的创建和销毁,极大地节省了内存和减少了垃圾回收(GC)的压力。

经验总结:

在缓存范围内[-128,127]使用自动装箱,总是会返回同一个对象的引用,此时"=="是可以达到预期结果的,但这只是一个优化实现的副作用,并非设计初衷,在超出缓存范围时就会出现意外.因此,比较值时用.equals更稳妥

当然,缓存上限是可以更改的,可以自行查询,这里不做说明

2. 泛型

2.1 写法

泛型=“参数化类型”

想象一下函数的形参实参:定义一个函数时,参数x和y是形参,当你调用函数add(2,3)时,你传递了具体的值,也就是实参.

泛型将这个思想应用到了类型上,允许类,接口,方法在定义时使用"类型参数",在使用时在指定具体的类型.

上例子!

需求:实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值.

在知道泛型之前,为了能让使数组中可以存放任意类型的数据,我们可以创建一个Object[]类型的数组,原因很简单,Object是所有类的子类,Object[]类型的数组可以存放任何类型的对象,包括包装类的,再结合上面我们讲的自动装箱,可以写出以下代码:

在这里插入图片描述

可以看到,该数组可以存储任何类型的对象,但是在从数组中取出元素的时候,必须手动进行强制类型转换,因为编译器不知道取出的对象具体实时什么类型;先不说对大量数据一个个进行强制类型转换是否现实的问题,一不留神就出错了:

在这里插入图片描述

一不小心转换错类型了,编译器是不会提醒的,但是运行时会抛出ClassCastException异常

而泛型正是为了克服Object方式的各种缺点而设计的,它能让你写出更加通用,安全,可复用的代码,避免强制类型转换和运行时的ClassCastException,那它是怎么写的呢?

在这里插入图片描述

写法:本例实现的是一个很基础的泛型类,写法的话,第一步先给类添加类型参数,然后将后面涉及到该值的类型由基本类型换成T就好.

泛型方法的写法演示:

public class Utils {// 泛型方法,<T> 声明在返回值前public static <T> void printArray(T[] array) {for (T item : array) {System.out.print(item + " ");}System.out.println();}
}// 使用
Integer[] nums = {1, 2, 3};
String[] strs = {"A", "B", "C"};Utils.printArray(nums);   // 1 2 3
Utils.printArray(strs);   // A B C

泛型接口:

public interface Generator<T> {T next();
}// 实现时指定具体类型
public class StringGenerator implements Generator<String> {private String[] data = {"A", "B", "C"};private int index = 0;@Overridepublic String next() {return data[index++ % data.length];}
}

需要注意泛型不能直接实例化!

你不能直接使用new T() 这样的写法,具体原因是Java的类型擦除机制(下一小节会讲),擦除之后JVM不知道T具体是什么类型,所以无法创建示例

对此,你可以先实例化一个其他类型的,后面进行强转

总结一下泛型的优势:

  1. 类型安全:在编译期捕获类型错误,避免运行时崩溃
  2. 消除强制转换:是代码更简洁,更清晰
  3. 明确意图:从类的定义中能清晰看出要存储什么类型的数据

2.2 实现机制—类型擦除

类型擦除指的是,Java的泛型只存在于编译期,在编译后的字节码文件中,所有于泛型相关的类型信息都会被擦除,替换为它们的原始类型,并在相应的地方插入强制类型转换

在这里插入图片描述

这是将字节码反汇编成人可读命令的截图,很多看不懂没关系,最起码能看到,出现了好几处Object,但是那个类型参数T却是一个没看到.

原因在于:编译器在编译期间会执行严格的类型检查,确保类型安全,一旦编译通过,生成的字节码不再包含泛型信息,对于JVM来说,myArr和myArr都被擦成了Integer和String的原始类型Object,类型完全相同.

这样看起来,怎么和自动装箱那么像呢,他俩对实际性能都没太大优化,毕竟只是作用于编译前,都是一些表面工作,真正运行的时候就看不到了.

泛型和自动装箱确实都是Java引入的语法糖,但它们对性能的影响和实际价值需要有更准确的认识:

  1. 泛型
  • 性能影响:无额外运行时开销(因为类型信息在运行时被擦除)
  • 主要价值:
    • 类型安全:编译期检查类型错误(避免ClassCastException)
    • 代码可读性:明确集合中元素的类型
    • 减少样板代码:无需手动强制转换
  1. 自动装箱/拆箱
  • 性能影响:有运行时开销,每次装箱和拆箱都会创建对象或调用方法,在循环中大量使用可能导致性能问题(频繁创建对象和内存开销).
  • 主要价值:
    • 基本类型和包装类型之间的自动转换
    • 提升代码可读性

2.3 什么是原始类型?

什么是原始类型?

原始类型是指擦除了泛型信息后的泛型类或接口.

当一个泛型类被使用但不提供参数时呈现出的状态就是原始类型,也就是字节码中被擦除类型之后的类型.

还是用MyArr这个例子,

在这里插入图片描述

直白地说,原生类型的意思就是:“我知道这个类是泛型,但是我选择忽略它的类型检查”,此时的MyArr就是MyArr的原始类型,使用起来和不使用泛型是一样的,可以传入任何杂七杂八的类型,接收时强制类型转换以及可能抛出ClassCastException异常.

那这么写干嘛?闲的?

  1. 当然不是,这样做的一个很重要的原因就在于—历史兼容性.Java在jdk 5.0之前没有泛型,所有的集合都是原始类型,当Java引入泛型时,为了向后兼容,必须保留原始类型.若突然移除原始类型,所有旧代码就都无法编译了
  2. 教育意义,原始类型可以帮助开发者更好地理解类型擦除的工作原理,懂得权衡兼容性和类型安全.
  3. 处理特殊场景,在一些特殊场景下只能用原始类型.
http://www.xdnf.cn/news/20161.html

相关文章:

  • 大语言模型推理的幕后英雄:深入解析Prompt Processing工作机制
  • 时序数据库IoTDB的六大实用场景盘点
  • 基于机器学习的缓存准入策略研究
  • 服务器异常磁盘写排查手册 · 已删除文件句柄篇
  • 安装与配置Jenkins(小白的”升级打怪“成长之路)
  • AI-Agent智能体提示词工程使用分析
  • leetcode212.单词搜索II
  • SQL优化与准确性提升:基于RAG框架的智能SQL生成技术解析
  • webrtc之高通滤波——HighPassFilter源码及原理分析
  • 正则表达式,字符串的搜索与替换
  • 【面试题】介绍一下BERT和GPT的训练方式区别?
  • Ansible 项目管理核心要点总结
  • 进程与线程详解, IPC通信与RPC通信对比,Linux前台与后台作业
  • Android入门到实战(八):从发现页到详情页——跳转、传值与RecyclerView多类型布局
  • 深度学习——ResNet 卷积神经网络
  • Python快速入门专业版(二):print 函数深度解析:不止于打印字符串(含10+实用案例)
  • Docker多阶段构建Maven项目
  • K8s资源管理:高效管控CPU与内存
  • React学习之路永无止境:下一步,去向何方?
  • Jmeter基础教程详解
  • STM32H750 RTC介绍及应用
  • 国产GEO工具哪家强?巨推集团、SEO研究协会网、业界科技三强对比
  • 用C++实现日期类
  • upload-labs通关笔记-第17关文件上传关卡之二次渲染jpg格式
  • 关于如何在PostgreSQL中调整数据库参数和配置的综合指南
  • Vue基础知识-脚手架开发-子传父(props回调函数实现和自定义事件实现)
  • Win11 解决访问网站525 问题 .
  • 【RK3576】【Android14】如何在Android kernel-6.1 的版本中添加一个ko驱动并编译出来?
  • Django 常用功能完全指南:从核心基础到高级实战
  • [光学原理与应用-401]:设计 - 深紫外皮秒脉冲激光器 - 元件 - 布拉格衍射在深紫外皮秒声光调制器(AOM)中的核心作用与系统实现