常用基础类库(Java API·完整版精讲)(四)

四、常用基础类库(Java API·完整版精讲)

Java API(Application Programming Interface)即Java应用程序编程接口,是JDK自带的大量核心类、接口、工具类集合,无需自定义开发,可直接导入使用。本板块全覆盖Java开发最常用、面试最高频、实战必备的基础类库,包含底层原理、全套常用方法、实操案例、性能对比、易错点与面试真题,是项目开发、代码简化的核心地基。

1. String 字符串家族(重中之重·开发使用率Top1)

字符串是Java最常用的数据类型,String家族包含String、StringBuilder、StringBuffer 三类,核心区别为可变性、线程安全、执行效率,是面试高频对比考点。

一、String 不可变字符串(核心基础)

1. 底层原理

JDK1.9之前: 底层基于 char[] 字符数组存储;

JDK1.9及以后: 优化为 byte[] 字节数组存储,节省内存空间。

核心特性:不可变 ,字符串一旦初始化,内容无法修改。所有字符串拼接、替换操作,都会生成新的字符串对象,原对象不会改变。

2. 不可变核心优势

  • 线程安全:不可变对象天然规避多线程并发修改问题

  • 字符串常量池复用:节省内存、减少重复对象创建

  • 作为HashMap键安全:哈希值固定,不会出现映射错乱

3. 两种创建方式与底层区别(面试必考)

(1)直接赋值:String s = "abc" :优先从字符串常量池查找,存在则直接复用,不存在则创建常量池对象,效率高、内存占用低

(2)new对象:String s = new String("abc"):强制在堆内存创建新对象,无论常量池是否存在,都会生成新对象,造成内存冗余,日常开发禁止滥用

4. 全套高频常用方法(实操必备)

  • 获取类:length() 获取字符串长度、charAt(int index) 获取指定索引字符、indexOf(String str) 正向查找子串索引、lastIndexOf(String str) 反向查找子串索引

  • 截取类:substring(int begin) 从指定位置截取到末尾、substring(int begin,int end) 区间截取(左闭右开)

  • 转换类:toUpperCase() 转大写、toLowerCase() 转小写、toCharArray() 转字符数组、valueOf(任意数据类型) 其他类型转字符串

  • 判断类:equals(Object obj) 内容比较(区分大小写)、equalsIgnoreCase() 忽略大小写比较、isEmpty() 判断是否为空串、contains(String str) 判断是否包含子串、startsWith(String prefix) 前缀匹配、endsWith(String suffix) 后缀匹配

  • 修改工具类:replace(old,new) 字符/字符串替换、trim() 去除首尾空格、split(String regex) 正则分割字符串

5. 高频易错点

  • == 比较的是字符串地址值 ,equals 比较的是内容,字符串内容判断必须用equals

  • 空串 "" 和 null 不同:空串是已创建对象、无内容;null 是未初始化、无对象,调用方法会空指针报错

  • 字符串循环拼接不建议用String,会产生大量废弃对象,造成内存浪费

6. String 不可变性【底层终极原理·面试绝杀】

(1)什么是不可变? String 一旦完成初始化,字符串内容、底层存储数组长度、内存地址永久不可修改 。任何拼接、替换、截取操作不会修改原字符串,只会在内存中生成一个全新的字符串对象,原对象原封不动、永久不变。

(2)底层为什么不可变?(源码级原理·必背)

  • 底层存储私有 :JDK9+底层使用 private final byte[] value 存储字符数组,数组被private私有+final修饰,外部无法获取、无法修改引用地址;

  • 无对外修改入口:String类没有提供任何可以修改底层数组内容的set/modify方法,彻底封死修改通道;

  • 类被final修饰 :String类本身被final修饰,禁止被继承,杜绝子类重写方法破坏不可变性;

  • 所有修改方法均返回新对象:replace、concat、substring 等所有修改API,底层都是新建数组、新建字符串对象,原对象永不改变。

(3)final在String中的三层作用(面试深挖)

  • final修饰:禁止继承,防止子类篡改字符串行为;

  • final修饰底层数组:保证数组引用地址不可变,数组指向永远固定;

  • final修饰变量:保证字符串对象赋值后引用不可二次指向。

7. 字符串常量池(String Pool)核心机制(必考)

字符串常量池是JVM为String单独开辟的特殊内存缓存区域,目的:减少重复对象、节约内存、提升复用效率,是String高性能的核心基石。

运行机制

  • 直接赋值 String s = "xxx":优先在常量池查找,存在则复用地址,不存在则存入常量池;

  • new String():永远直接在堆内存新建对象,不复用常量池,产生内存冗余;

  • 常量池中的字符串对象全局唯一、不可重复、永久不可变

8. 字符串拼接底层原理(高频坑点)

(1)常量+常量拼接(编译期优化)

例:String s = "a" + "b"; 底层:编译器编译时直接优化为 "ab",直接走常量池,无性能损耗。

(2)变量+常量 / 变量+变量拼接(运行期新建对象)

例:String s1 = "a"; String s2 = s1 + "b"; 底层:JVM自动创建 StringBuilder 进行拼接,最后toString()生成新String对象,产生新堆对象,频繁循环拼接极度浪费内存。

(3)final常量变量拼接

被final修饰的字符串变量,编译期直接解析为常量,拼接同样触发编译优化,不产生多余对象。

9. intern() 方法底层机制(进阶面试)

作用 :主动将堆内存字符串对象放入/复用常量池,强制复用常量池对象,节约内存。

  • 如果常量池已有相同内容:返回常量池地址;

  • 如果常量池无内容:将当前堆对象存入常量池,复用地址。

10. String 不可变性四大核心优势(满分背诵)

  • 线程安全:对象不可修改,多线程并发场景不会出现读写冲突,天然线程安全;

  • 常量池复用:内容固定可缓存,大量减少重复对象,极大节省JVM内存;

  • 哈希值稳定:创建时hashCode一次性计算缓存,永不改变,适合作为HashMap、HashSet的key,不会出现映射丢失、查找错乱;

  • 数据安全:网络参数、配置信息、密码字符串不可被篡改,底层数据安全性高。

11. 高频终极易错点(大厂笔试坑点)

坑1:String不可变 = 引用不可变?

错!String内容不可变,但引用变量可以重新赋值指向新对象,只是原对象内容永远不变

坑2:substring 截取会修改原字符串?

错!截取只会返回新字符串对象,原字符串完全不变。

坑3:循环拼接用String没问题?

错!循环中每一次拼接都会新建对象,产生大量GC垃圾,循环拼接必须用StringBuilder。

坑4:new String 会优先走常量池?

错!new 永远堆新建,只有双引号直接赋值才走常量池。

12. String 高频面试真题(直接背诵)

Q1:为什么String要设计成不可变?

A:为了保障线程安全、支持字符串常量池复用、保证哈希值永久稳定适配集合键、保证底层数据不可篡改,从底层提升Java程序稳定性与内存利用率。

Q2:String为什么要用final修饰?

A:final修饰类禁止继承,防止子类破坏不可变逻辑;final修饰底层数组保证引用固定,从语法、源码、继承三层彻底锁死不可变性。

Q3:字符串拼接哪种方式效率最高?为什么?

A:单常量拼接编译优化效率最高;循环拼接优先StringBuilder。变量拼接每次生成新String对象,产生大量垃圾内存,效率极低。

Q4:String s = new String("abc") 会创建几个对象?

A:两个对象。一个是常量池中的"abc"常量对象,一个是堆内存new出来的String实例对象。

二、StringBuilder 可变字符串(日常开发首选·满分精讲)

1. 核心定义与本质特性(必背) StringBuilder是可变字符序列 ,底层依托动态可扩容的 byte[] 数组(JDK9+)存储内容,对象创建后可以直接修改原数组内容 ,不会像String一样频繁新建对象。 核心标签:可变、无锁、线程不安全、执行效率极高、单线程绝对首选

2. 底层扩容机制(面试高频) StringBuilder默认无参构造初始容量为 16,即默认创建长度16的空字节数组。

扩容触发规则:当存储字符长度超出数组容量时,自动触发底层扩容。

扩容算法:新容量 = 原容量 * 2 + 2; 扩容流程:新建更大容量数组 → 复制原数组内容 → 废弃旧数组,全程自动完成,对开发者透明。

开发优化 :已知字符串大概长度,建议带参构造指定初始容量,避免多次扩容、节省性能

3. 为什么线程不安全?(面试必问)

StringBuilder所有增删改方法无synchronized同步锁,多线程并发拼接时会出现:

  1. 数组覆盖写入、数据丢失;

  2. 计数变量count错乱;

  3. 最终字符串内容残缺、重复、异常。

结论:仅单线程使用,多线程禁止直接使用StringBuilder

4. 全套高频核心方法(实操全覆盖)

  • append() 末尾拼接:支持字符串、数字、布尔、字符等所有类型,最核心高频方法,直接修改原对象,不产生新对象

  • insert(int offset,数据) 指定位置插入:在指定下标位置插入内容,实现中间追加

  • delete(int start,int end) 区间删除:左闭右开区间删除字符,精准删除片段内容

  • deleteCharAt(int index) 单点删除:删除指定单个下标字符

  • replace(int start,int end,String str) 区间替换:替换指定区间为新内容

  • reverse() 字符串反转:原地反转字符序列,直接修改原对象

  • length() 获取有效长度:返回当前实际字符个数,非数组容量

  • capacity() 获取数组容量:获取底层数组总容纳长度(含空闲位置)

  • toString() 转为String:将可变字符串转为不可变String对象,开发收尾必备操作

5. StringBuilder 与 String 性能核心差距(背诵)

  • String修改:每次拼接/修改新建对象,堆内存大量产生垃圾对象、频繁GC,循环场景性能极差

  • StringBuilder修改:全程操作同一个数组、同一个对象,仅扩容时少量复制,无大量GC,循环拼接性能提升数十倍

6. 企业开发强制规范(落地准则)

  • 固定字符串、极少修改:使用String

  • 循环拼接、批量组装、动态字符串:强制使用StringBuilder(代码审核必查点)

  • 单线程环境:优先StringBuilder,舍弃StringBuffer

  • 预估数据量大:手动指定初始容量,规避多次扩容损耗

  • 禁止循环内new StringBuilder:循环外创建、循环内append、循环结束toString,性能最优

7. 高频易错坑点(笔试避坑)

坑1 :误以为StringBuilder修改会产生新对象 纠正:所有修改原地修改原对象,引用地址永远不变

坑2:多线程全局共享StringBuilder 纠正:线程不安全,并发场景数据错乱,必须用StringBuffer或线程局部变量

坑3:混淆length()和capacity() 纠正:length是实际字符数,capacity是底层数组总容量

坑4:拼接完不调用toString()直接使用 纠正:业务接收参数大多是String类型,必须转为不可变字符串对外返回

8. 面试真题满分问答(直接背诵)

Q1:StringBuilder为什么效率远高于String循环拼接?

A:String每次修改都会创建新字符串对象,产生大量GC垃圾;StringBuilder底层是可变数组,原地修改内容,不频繁新建对象,仅扩容时少量复制,极大减少内存开销与GC次数,循环场景性能碾压String。

Q2:StringBuilder初始容量和扩容规则?

A:无参构造默认初始容量16;容量不足时自动扩容,扩容公式:新容量 = 原容量 * 2 + 2,底层数组复制完成扩容。

Q3:为什么StringBuilder线程不安全?什么场景会出问题?

A:方法无同步锁,多线程并发append、修改时会出现数组覆盖、计数错乱、数据丢失问题。多线程并发字符串拼接场景禁止使用。

Q4:开发中如何最大化优化StringBuilder性能?

A:预估字符串长度手动指定初始容量,避免多次扩容;将对象创建在循环外部,复用同一个拼接对象;减少无效insert、delete操作,优先尾部append。

三、StringBuffer 可变字符串(多线程专属·面试精讲完整版)

1. 核心定义与本质特性(必背)

StringBuffer 属于可变字符序列 ,底层同样采用动态可扩容的 byte[] 数组(JDK9+)存储字符内容,支持原地增删改,不会频繁创建新对象。

核心标签:可变、带同步锁、线程安全、执行效率偏低、多线程并发专属

2. 线程安全底层原理(面试必考)

StringBuffer 的所有增、删、改、拼接方法(append/delete/insert/replace等)

全部被 synchronized 关键字修饰 。 锁粒度:对象级同步锁,保证同一时刻只有一个线程操作当前StringBuffer对象并发效果:杜绝多线程覆盖写入、count计数错乱、数据丢失问题代价:每次方法调用都要加锁、解锁、竞争资源,产生线程阻塞开销,执行效率远低于StringBuilder

3. 底层扩容机制(和StringBuilder完全一致)

  • 无参构造默认初始容量:16

  • 扩容触发条件:当前字符长度超出底层数组容量

  • 扩容算法:新容量 = 原容量 * 2 + 2

  • 扩容流程:新建大容量数组 → 复制旧数据 → 废弃旧数组,全程透明

  • 性能优化:多线程批量拼接场景,同样建议手动指定初始容量,减少频繁扩容损耗

4. 全套高频核心方法(与StringBuilder完全通用)

StringBuffer与StringBuilder方法API完全一致,具备所有可变字符串操作能力,可无缝切换:

  • append() 尾部拼接(支持全类型)

  • insert() 指定位置插入

  • delete() / deleteCharAt() 区间/单点删除

  • replace() 区间内容替换

  • reverse() 字符串反转

  • length() 获取有效字符长度

  • capacity() 获取底层数组总容量

  • toString() 转为不可变String对象

5. StringBuffer 性能短板(面试深挖)

  • 单线程严重低效:单线程无并发竞争,加锁属于无效开销,相比StringBuilder性能下降明显

  • 高并发阻塞严重:多线程高频写入场景,线程大量排队阻塞,吞吐量低

  • 锁无法精细化控制:全方法粗粒度锁,无法按需加锁,灵活性差

6. 精准适用场景(企业强制规范)

✅ 必须使用 StringBuffer 的场景

  • 多线程并发环境下,全局共享字符串变量频繁拼接、修改

  • 线程池任务内共享字符串组装、批量数据拼接

  • 老旧项目兼容代码、传统同步业务模块

❌ 禁止使用场景

  • 所有单线程业务(接口业务、循环拼接、本地变量组装),一律用StringBuilder

  • 追求高吞吐、高性能的字符串拼接场景

7. 高频易错坑点(避坑满分)

坑1:误以为加锁就绝对万无一失 纠正:synchronized仅保证方法操作原子性,不保证多线程组合业务逻辑原子性,复杂并发逻辑仍需手动加锁控制

坑2:单线程业务滥用StringBuffer 纠正:产生大量无效锁竞争,拖慢接口性能,属于典型不规范编码

坑3:混淆length()与capacity() 纠正:length是实际字符数,capacity是底层数组容量,扩容只改变capacity,不影响length

坑4:多线程局部变量使用StringBuffer 纠正:局部变量不共享、无并发竞争,没必要用,优先StringBuilder

8. 面试满分真题问答(直接背诵)

Q1:StringBuffer 为什么线程安全?和StringBuilder核心区别?

A:StringBuffer所有修改方法被synchronized修饰,具备对象同步锁,多线程并发不会出现数据错乱,线程安全但效率低;StringBuilder无锁、无并发保障,线程不安全、效率极高。二者API、底层扩容机制完全一致,唯一差异就是同步锁与线程安全

Q2:既然StringBuffer线程安全,为什么不全程使用?

A:同步锁会带来加锁、解锁、线程阻塞的性能开销,单线程场景完全没必要,会造成性能浪费;企业开发遵循「无并发不用锁」原则,单线程首选StringBuilder,仅多线程共享场景使用StringBuffer。

Q3:StringBuffer 能否解决高并发大量拼接的性能问题?

A:不能。StringBuffer是粗粒度方法锁,高并发下线程大量阻塞排队,吞吐量极低;超高并发场景推荐ThreadLocal绑定StringBuilder,比StringBuffer性能高数十倍。

Q4:三者底层扩容机制是否一致?

A:完全一致。StringBuilder与StringBuffer底层数组结构、默认初始容量、扩容算法一模一样,唯一区别就是是否加同步锁。

四、三者终极对比与选型规范(面试满分考点·必背终版)

本节汇总 String / StringBuilder / StringBuffer 底层差异、线程特性、性能优劣、落地场景、面试标准答案,是Java基础笔试、面试、代码规范审查高频核心考点,全篇内容可直接背诵。

1. 全维度终极对比表(最全维度)

|----------|---------------------------------|------------------------|------------------------|
| 对比维度 | String | StringBuilder | StringBuffer |
| 可变性 | 不可变(底层final数组) | 可变(动态扩容数组) | 可变(动态扩容数组) |
| 线程安全 | 线程安全(不可变天然安全) | 线程不安全(无锁) | 线程安全(synchronized锁) |
| 执行效率 | 单次操作快,频繁修改极慢 | 三者中效率最高 | 效率最低(加锁解锁开销) |
| 底层存储 | private final byte\[\]/char\[\] | 动态可扩容byte\[\]/char\[\] | 动态可扩容byte\[\]/char\[\] |
| 扩容机制 | 无扩容(每次修改新建对象) | 默认16,扩容公式:原容量*2+2 | 默认16,扩容公式:原容量*2+2 |
| 锁机制 | 无锁 | 无任何同步锁 | 方法级synchronized对象锁 |
| 对象复用 | 支持常量池复用 | 不支持常量池 | 不支持常量池 |
| 修改特点 | 原对象不变,返回新对象 | 原地修改原对象,无新对象 | 原地修改原对象,无新对象 |
| 适用环境 | 静态固定字符串、常量 | 单线程、频繁拼接修改 | 多线程并发共享修改 |

2. 企业开发强制选型标准(落地必守)

✅ 强制使用 String

  • 字符串内容固定不变、极少修改的场景

  • 定义系统常量、配置参数、固定文本

  • 作为HashMap、HashSet的Key(哈希值稳定、安全)

  • 方法返回固定文本、静态资源描述

✅ 强制使用 StringBuilder(日常开发首选)

  • 所有单线程动态字符串拼接(接口开发、业务组装、循环拼接)

  • for/while 循环内多次字符串拼接(杜绝大量GC垃圾)

  • 动态组装SQL、报文、日志、模板文本

  • 仅局部变量使用,不存在多线程共享场景

✅ 强制使用 StringBuffer(极少用、仅限并发)

  • 多线程共享全局字符串变量,高频并发拼接修改

  • 线程池任务批量组装共享文本数据

  • 老旧遗留项目同步代码兼容场景

❌ 企业编码禁止行为

  • 禁止循环拼接使用String直接相加(性能灾难)

  • 禁止单线程业务滥用StringBuffer(无效锁开销)

  • 禁止将StringBuilder定义为全局静态变量(线程不安全)

3. 终极选型背诵口诀(面试秒答)

固定不变用String,单拼多改用Builder,并发共享用Buffer。

4. 高频笔试易错坑点总结

坑1 :三者扩容机制不完全一致 纠正:StringBuilder与StringBuffer扩容机制完全一致,默认16、扩容公式一致,唯一区别是锁

坑2:String不可变代表引用不可变 纠正:内容不可变,引用变量可重新赋值指向新对象

坑3:多线程局部变量需要用StringBuffer 纠正:局部变量线程独享,无并发竞争,优先StringBuilder

坑4 :String拼接效率一定最低 纠正:纯常量拼接 编译期优化,效率极高;变量循环拼接效率极低

5. 面试满分标准问答(终版背诵)

Q1:简述 String、StringBuilder、StringBuffer 三者区别?

A:

  1. 可变性:String 底层被final修饰,不可变;后两者底层为动态扩容数组,可变。

  2. 线程安全:String天然线程安全;StringBuilder无锁、线程不安全;StringBuffer方法加synchronized锁、线程安全。

  3. 性能:String单次常量拼接快,频繁修改极慢;StringBuilder效率最高;StringBuffer因加锁开销效率最低。

  4. 场景:固定文本用String,单线程动态拼接用StringBuilder,多线程并发共享修改用StringBuffer。

Q2:为什么循环拼接不建议使用String?

A:String每次修改都会生成新字符串对象,循环场景下会产生大量废弃对象,触发频繁GC,严重降低接口性能;而StringBuilder原地修改数组、不新建对象,循环场景性能远优于String。

Q3:StringBuffer线程安全为什么不推荐日常使用?

A:StringBuffer依靠方法级同步锁实现线程安全,存在大量加锁、解锁、线程阻塞的性能开销;日常单线程业务无并发竞争,加锁属于无效损耗,违背高性能开发原则,仅多线程共享场景才有使用价值。

Q4:如何解决高并发场景字符串拼接性能问题?

A:不推荐使用低效的StringBuffer;最优方案是 ThreadLocal绑定StringBuilder,每个线程独享一个拼接对象,既规避并发安全问题,又保留StringBuilder高性能优势,性能远超StringBuffer。

Q5:String 为什么适合做HashMap的Key?

A:String不可变,哈希值创建后永久固定,不会出现key哈希值变动导致的映射丢失、查找异常;同时支持常量池复用,内存利用率高。

2. 基本数据类型包装类(面试核心·自动装箱拆箱)

Java为8种基本数据类型提供对应的包装类,将基本类型封装为对象类型,弥补基本类型无法面向对象、无法赋值为null、无法存储在集合中的短板,位于java.lang包下,默认无需导包。

一、八大包装类完整对应关系(必背表格+规律)

|---------------|-----------|----------|---------------------|
| 基本数据类型 | 对应包装类 | 所属体系 | 缓存范围 |
| byte(字节型) | Byte | 数值型 | -128 ~ 127 |
| short(短整型) | Short | 数值型 | -128 ~ 127 |
| int(整型) | Integer | 数值型 | -128 ~ 127(可修改上限) |
| long(长整型) | Long | 数值型 | -128 ~ 127 |
| float(单精度浮点) | Float | 浮点型 | 无缓存 |
| double(双精度浮点) | Double | 浮点型 | 无缓存 |
| char(字符型) | Character | 字符型 | 0 ~ 127(ASCII基础字符) |
| boolean(布尔型) | Boolean | 布尔型 | true / false 全局缓存 |

1. 命名背诵规律(永久不记错)

  • 特殊两组(唯一需要单独记忆):int → Integer、char → Character

  • 通用规则(其余全适配):基本类型首字母大写 = 对应包装类(byte→Byte、short→Short、long→Long等)

2. 核心层级体系(面试拓展)

  • 所有数值型包装类(Byte/Short/Integer/Long/Float/Double)直接继承 Number抽象类

  • 所有包装类默认继承Object ,且全部被 final修饰、不可继承、不可变

  • 包装类对象一旦创建,内部数值不可修改,和String一样具备不可变性

3. 基本类型 vs 包装类 核心差异(必背)

  • 默认值不同 :基本类型有默认值(int=0、boolean=false);包装类默认值为 null

  • 存储方式不同:基本类型直接存储数值,占用内存小;包装类是对象,存储堆内存地址

  • 泛型/集合限制 :集合、泛型只能存包装类,不支持基本类型(如List<Integer>,不能写List<int>)

  • 空值能力:基本类型无法表示空值;包装类可赋值null,适配数据库字段空值场景

4. 缓存机制终极总结(笔试高频)

  • 有缓存的包装类:Byte、Short、Integer、Long、Character、Boolean

  • 无缓存的包装类:Float、Double(浮点数值无穷多,无法缓存)

  • 缓存本质:静态内部类提前初始化常用对象,实现复用、减少GC、节约内存

5. 高频易错坑点(避坑)

坑1:浮点型Float、Double没有缓存,无论数值大小,== 比较永远不相等

坑2:new 关键字创建的包装类对象,永远不走缓存,地址一定不同

坑3:包装类是不可变类,修改数值本质是新建包装对象

坑4:数据库int字段可空时,实体类必须用Integer,不能用int(基本类型无法存null)

6. 企业开发强制规范

  • 实体类属性、集合存储、接口参数:统一使用包装类(支持null、适配数据库空值)

  • 方法内部局部变量、纯数值计算:优先使用基本类型(节省内存、效率更高)

核心规律:除int→Integer、char→Character外,其余包装类均为基本类型首字母大写。

二、核心概念:自动装箱与自动拆箱(JDK5+新特性·面试必考)

1. 核心定义(必背)

自动装箱 :JDK5新增语法特性,编译器自动将基本数据类型 转换为对应的包装类对象 ,无需手动调用方法,底层默认通过 包装类.valueOf() 实现。

示例:Integer num = 10; // 自动装箱,等价于 Integer num = Integer.valueOf(10);

自动拆箱 :编译器自动将包装类对象 转换为对应的基本数据类型 ,底层默认通过 包装类.xxxValue() 实现。

示例:int n = num; // 自动拆箱,等价于 int n = num.intValue();

2. 底层编译原理(核心考点)

  • 装箱、拆箱属于编译期优化,不是运行期机制:代码编译阶段编译器自动补全对应方法,运行时无额外开销。

  • 自动装箱优先走缓存 :以Integer为例,装箱时优先调用 Integer.valueOf(),优先复用缓存池对象,只有超出缓存范围才会新建对象,节省内存。

  • 自动拆箱无缓存概念:拆箱是直接获取包装类内部的基本类型数值,只做数值转换,不涉及对象创建与复用。

3. 完整代码演示

java 复制代码
// 1. 自动装箱
Integer a = 88;  // 编译后:Integer a = Integer.valueOf(88);

// 2. 自动拆箱
int b = a;       // 编译后:int b = a.intValue();

// 3. 运算触发自动拆箱
Integer c = 100;
int d = c + 20;  // c先拆箱为基本类型100,再做数值运算

4. 触发装箱/拆箱的常见场景(笔试高频)

  • 自动装箱场景:基本类型赋值给包装类、基本类型存入集合、基本类型传入包装类参数方法

  • 自动拆箱场景:包装类赋值给基本类型、包装类参与数学运算、包装类比较数值大小

5. 核心优缺点总结

  • 优点:极大简化代码,无需手动完成类型转换,适配集合存储、泛型语法,实现基本类型与对象类型无缝衔接

  • 缺点 :频繁转换会产生多余对象、轻微损耗性能;空包装类拆箱会触发空指针异常

6. 终极高频易错坑点(避坑满分)

坑1:包装类null拆箱报错

纠正:Integer a = null; int b = a; 直接空指针!null对象无法调用intValue(),业务中必须先判空再运算。

坑2:== 比较混用装箱拆箱导致结果迷惑

纠正:基本类型==比数值、包装类==比地址;混用时包装类会自动拆箱比数值。

例:Integer a=127; int b=127; a==b → true(a拆箱数值对比)

坑3:循环内频繁装箱产生大量对象

纠正:循环中频繁赋值包装类,会不断触发valueOf,超出缓存范围会大量新建对象,造成GC压力。

坑4:混淆new对象装箱与自动装箱

纠正:自动装箱优先走缓存;new Integer() 强制新建对象,永远不走缓存。

7. 面试满分真题问答(直接背诵)

Q1:什么是自动装箱与自动拆箱?底层原理是什么?

A:JDK5提供的语法特性,自动装箱是基本类型转包装类,底层调用 valueOf() 方法,优先复用缓存对象;自动拆箱是包装类转基本类型,底层调用 xxxValue() 方法。二者均为编译期自动优化,简化类型转换代码。

Q2:包装类拆箱为什么会出现空指针异常?

A:当包装类变量为null时,自动拆箱会隐性调用对象的xxxValue()方法,null对象无法执行实例方法,直接抛出空指针异常,是开发中高频空指针来源。

Q3:自动装箱和手动new包装类有什么区别?

A:自动装箱通过valueOf()实现,优先使用缓存池对象,内存利用率高;new包装类强制在堆内存新建对象,不使用缓存,内存开销大,日常开发优先使用自动装箱。

Q4:为什么尽量避免在循环中频繁进行包装类装箱转换?

A:超出缓存区间的装箱会频繁创建新包装对象,产生大量临时垃圾对象,增加JVM GC压力,降低程序运行性能。

三、Integer缓存池(面试高频必考·源码级终版)

1. 核心定义与默认缓存规则(必背)

Integer缓存池又称 IntegerCache ,是Integer内部的静态私有内部类 ,JVM启动时自动初始化,提前批量创建并缓存高频整数对象,目的是复用对象、减少频繁创建销毁、降低GC压力、提升运行效率

默认缓存区间-128 ~ 127

核心规则

  • 数值在 -128~127 区间:自动装箱直接复用缓存池中已有对象,地址完全相同

  • 数值超出 -128~127 区间:自动装箱新建Integer对象,不走缓存

  • new Integer() 构造方法:强制新建堆对象,无论数值是否在缓存区间,永远不走缓存

2. 底层源码核心原理(面试深挖)

IntegerCache 是Integer私有静态内部类,JDK加载时执行静态代码块,一次性初始化缓存数组:

  • 最小值固定:low = -128(不可修改)

  • 最大值默认:high = 127(可通过JVM参数修改)

  • 底层实现:初始化 Integer[] cache 数组,预存区间内所有Integer对象

  • 执行时机:类加载阶段一次性初始化,全局复用,全程只创建一次

3. 缓存区间自定义配置(进阶面试点)

Integer缓存池上限可修改、下限不可改,可通过JVM启动参数调整缓存最大值:

-XX:AutoBoxCacheMax=256

配置后缓存区间变为:-128 ~ 256,适合大批量整数高频复用场景,默认无需修改。

4. 八大包装类缓存统一对照(串联考点)

  • 支持缓存:Byte、Short、Integer、Long(固定-128~127);Character(0~127);Boolean(true/full全局缓存)

  • 不支持缓存:Float、Double(浮点数值无限连续,无法预缓存)

5. 经典笔试真题代码辨析(满分避坑)

java 复制代码
// 案例1:区间内自动装箱 ------ 走缓存,同一对象
Integer a1 = 127;
Integer a2 = 127;
System.out.println(a1 == a2); // true

// 案例2:区间外自动装箱 ------ 新建对象,地址不同
Integer b1 = 128;
Integer b2 = 128;
System.out.println(b1 == b2); // false

// 案例3:new对象永远不走缓存
Integer c1 = new Integer(100);
Integer c2 = 100;
System.out.println(c1 == c2); // false

// 案例4:包装类与基本类型对比 ------ 自动拆箱比数值
Integer d1 = 127;
int d2 = 127;
System.out.println(d1 == d2); // true

6. 四大高频易错坑点(笔试必避)

坑1:以为128也会走缓存 纠正:默认最大缓存127,128及以上全部新建对象,== 对比为false

坑2:new Integer() 认为可以复用缓存 纠正:new 关键字强制在堆内存创建新对象,和缓存池无关

坑3:混淆 == 和 equals 纠正:Integer == 比较地址,equals 比较数值;判断数值永远用equals()

坑4:以为缓存区间上下限都可改 纠正:仅上限可通过JVM参数修改,下限固定-128不可更改

7. 面试满分真题问答(终版背诵)

Q1:简述Integer缓存池原理和作用?

A:Integer通过私有静态内部类IntegerCache实现缓存,默认预加载-128~127区间的Integer对象,全局复用常用数值对象。核心作用是减少高频整数对象的重复创建、节省堆内存、减少GC触发、提升程序运行效率。

Q2:为什么Integer缓存区间是-128到127?

A:该区间是程序中整数最高频使用区间,覆盖绝大多数日常数值运算、计数、状态码场景,缓存性价比最高;下限固定保证底层稳定,上限支持动态扩展适配业务场景。

Q3:new Integer(10) 和 Integer.valueOf(10) 区别?

A:new Integer() 强制新建堆对象,不使用缓存,内存开销大;Integer.valueOf() 自动优先走缓存,区间内复用对象,效率更高,日常自动装箱底层默认调用valueOf()。

Q4:开发中Integer数值比较规范写法是什么?

A:杜绝使用 == 比较包装类地址,统一使用 equals() 方法比较数值,彻底规避缓存区间导致的真假结果不一致问题。

四、通用核心方法
  • valueOf():基本类型/字符串转包装类

  • parseXXX(String str):字符串转对应基本类型(如Integer.parseInt())

  • xxxValue():包装类转基本类型

注意:包装类默认值为null,基本类型有默认初始值,集合存储数值必须使用包装类。

3. 核心工具类(Math、Random、System、Runtime)

四类工具类均为构造方法私有、静态方法全局调用,无需创建对象,直接类名.方法名调用,是日常数值运算、随机数生成、系统操作的基础工具。

一、Math 数学工具类(数值运算·面试+实操完整版)

1. 类核心特性(必背)

  • 所属包:java.lang,默认无需手动导包,直接使用

  • 构造方法:私有私有化 ,禁止实例化对象,所有属性和方法均为 static 静态

  • 核心作用:提供标准数学运算、数值取舍、最值计算、随机数、幂次开方、三角函数等工具能力

  • 底层特点:方法执行效率高,底层基于native本地方法+Java基础运算实现,无对象创建开销

2. 两大核心静态常量(开发常用)

  • Math.PI :圆周率常量,近似值 3.141592653589793,用于圆、扇形、几何计算

  • Math.E :自然常数,近似值 2.718281828459045,用于指数、对数、科学计算

3. 全套高频核心方法(含代码案例·实操全覆盖)

(1)绝对值计算 abs()

  • 支持参数:int、long、float、double 四种数值类型

  • 作用:返回参数的绝对值,负数转正数,正数不变

  • 案例: Math.abs(-10); // 10 ``Math.abs(3.14); // 3.14

(2)最值计算 max() / min()

  • 作用:对比两个数值,返回最大值/最小值,支持四大数值类型

  • 案例: Math.max(10,20); // 20 ``Math.min(5.6,2.8); // 2.8

  • 拓展:如需多个数值最值,可嵌套调用或结合数组遍历

(3)数值取舍(笔试高频坑点)

1.round() 四舍五入:小数部分≥0.5进1,否则舍去,返回long/int整数

Math.round(3.4); // 3

Math.round(3.5); // 4

Math.round(-3.5); // -3(负数四舍五入特殊规则:靠近0取值)

2.ceil() 向上取整:无论小数多少,直接向大数取值,返回double

Math.ceil(3.1); // 4.0

Math.ceil(-3.9); // -3.0

3.floor() 向下取整:无论小数多少,直接向小数取值,返回

double Math.floor(3.9); // 3.0

Math.floor(-3.1); // -4.0

(4)幂运算与开方运算

  • pow(double a,double b) :计算a的b次幂 Math.pow(2,3); // 8.0(2的3次方)

  • sqrt(double a) :计算平方根(开二次方) Math.sqrt(16); // 4.0

  • cbrt(double a) :计算立方根(JDK1.5+) Math.cbrt(8); // 2.0

(5)随机数生成 random()

  • 返回值:[0.0, 1.0) 区间double随机小数(包含0,不包含1)

  • 底层原理:底层依赖Random类实现,线程安全但高并发性能一般

  • 常用拓展公式(实操必备):

// 生成 [0,n) 随机整数 ``(int)(Math.random() * n);

// 生成 [min,max] 随机整数 ``(int)(Math.random() * (max - min + 1)) + min;

(6)符号与数值处理(进阶方法)

  • signum(double d):获取数值符号,正数返回1.0、负数返回-1.0、0返回0.0

  • rint(double d):就近取整,.5时取偶数,解决round负数歧义问题

4. 高频笔试易错坑点(满分避坑)

坑1:混淆三种取整规则

纠正:round是四舍五入;ceil向上取整、floor向下取整,负数取值方向极易出错,重点记忆负数案例。

坑2:Math.random() 无法直接生成整数

纠正:默认生成小数,必须强制类型转换+区间运算才能获取随机整数。

坑3:pow、sqrt返回值均为double

纠正:即使结果为整数,返回值也是浮点类型,如需整数需手动强转。

坑4:负数四舍五入想当然

纠正:Math.round(-3.5) 结果为-3,不是-4,规则为向0靠拢

5. 开发选型规范

  • 简单数学计算、快速随机数生成:优先使用Math工具类,代码简洁无对象开销

  • 高并发大量随机数、自定义随机规则:优先使用Random类,性能更优

  • 高精度金融计算:禁止使用Math浮点运算,需使用 BigDecimal

6. 面试简答真题(直接背诵)

Q1:Math类为什么不能实例化?

A:Math类构造方法被私有修饰,所有功能均由静态常量和静态方法实现,无需创建对象,直接通过类名调用,节省内存开销。

Q2:round、ceil、floor三者取整区别?

A:round是四舍五入取整;ceil是向上取整,向更大数值靠拢;floor是向下取整,向更小数值靠拢,三者对负数的取值结果差异极大,是笔试高频考点。

Q3:Math.random() 和 Random类区别?

A:Math.random() 底层基于Random实现,使用简单、适合少量随机数;Random类功能更丰富,支持自定义随机种子、批量生成随机数,高并发场景性能更稳定。

二、Random 随机数工具类(实操高频·面试精讲完整版)

1. 类核心特性(必背)

  • 所属包:java.util.Random,需要手动导包使用

  • 核心定位:专门用于生成各类伪随机数,支持整数、小数、布尔、字节数组等多类型随机值

  • 底层原理:伪随机数生成器,通过固定种子算法计算生成随机数,并非真正物理随机

  • 线程特性:线程不安全,多线程高并发场景存在性能瓶颈,推荐使用ThreadLocalRandom

  • 对比Math.random():Math.random()底层直接调用Random.nextDouble(),Random功能更全面、可自定义种子、可控性更强

2. 两大构造方法(核心考点·种子机制)

Random核心依靠**种子(seed)**生成随机数,种子相同,随机数序列完全一致

  • 空参构造 Random() :默认使用系统时间戳作为随机种子,每次运行随机数不同,日常开发首选

  • 有参构造 Random(long seed):自定义固定种子,种子固定则生成的随机数序列永久固定,适合测试、固定随机场景

java 复制代码
// 空参构造:随机种子,每次结果不同(常用)
Random random = new Random();

// 有参构造:固定种子,每次运行随机数完全一致(测试场景)
Random fixRandom = new Random(100);

3. 全套高频核心方法(含实操代码案例)

(1)整数随机数(最常用)

1) nextInt():生成整个int取值范围内的随机整数(正负都有)

2) nextInt(int bound) :生成 [0, bound) 区间非负随机整数,左闭右开,开发最高频方法

Random r = new Random(); ``r.nextInt(); // 随机int整数(正负)

r.nextInt(10); // 0~9 随机整数

(2)小数随机数

  • nextDouble() :生成 [0.0,1.0) 随机小数,和Math.random()效果一致

  • nextFloat() :生成 [0.0,1.0) 单精度随机小数

(3)布尔随机数

  • nextBoolean():随机生成 true / false,概率各50%

(4)字节数组随机填充

  • nextBytes(byte\[\] b):随机填充字节数组,多用于验证码、密钥生成场景

4. 自定义区间随机数公式(实操必备)

Random原生仅支持从0开始的区间,自定义区间需手动运算,两套万能公式:

  • 生成 [min, max) 随机整数:r.nextInt(max - min) + min

  • 生成 min, max 随机整数:r.nextInt(max - min + 1) + min

Random r = new Random(); // 生成 1~10 闭区间随机整数

int num = r.nextInt(10 - 1 + 1) + 1;

5. Random 与 Math.random() 终极对比

|----------|-----------------------|---------------------------|
| 对比维度 | Random工具类 | Math.random() |
| 功能丰富度 | 支持整数、布尔、数组、自定义种子,功能全面 | 仅能生成[0,1)小数,功能单一 |
| 底层实现 | 独立伪随机算法 | 底层直接调用Random.nextDouble() |
| 可控性 | 可自定义种子、固定随机序列 | 无种子控制,无法固定随机结果 |
| 适用场景 | 复杂随机、多类型随机、测试固定随机 | 简单快速生成随机小数/少量随机数 |

6. 高频易错坑点(笔试+实操避坑)

坑1:误解随机为真随机

纠正:Random是伪随机,基于种子算法计算,种子固定则随机序列完全固定,并非绝对随机。

坑2:循环内频繁new Random对象

纠正:频繁创建随机对象会复用相近时间戳种子,导致随机数大量重复;全局创建一个Random对象,循环内复用

坑3:混淆左右区间

纠正:nextInt(n)是左闭右开 [0,n),无法取到n本身,取闭区间必须+1修正。

坑4:多线程使用普通Random

纠正:Random线程不安全,多线程竞争会导致随机数重复、性能暴跌,高并发必须用ThreadLocalRandom。

7. 进阶优化:ThreadLocalRandom(高并发首选)

  • JDK7+ 提供,位于java.util.concurrent包

  • 核心优势:线程安全、无锁、高并发性能远超Random

  • 使用方式:ThreadLocalRandom.current().nextInt(min,max)

  • 企业规范:单线程用Random,多线程高并发强制用ThreadLocalRandom

8. 面试满分真题问答(直接背诵)

Q1:Random的随机原理是什么?是真随机吗?

A:Random基于种子+固定算法生成伪随机数,并非物理真随机。空参构造以系统时间戳为种子,有参构造可自定义种子,种子相同则生成的随机数序列完全一致。

Q2:为什么不建议在循环内频繁new Random?

A:Random空参构造依赖系统时间戳作为种子,循环内频繁创建对象会出现种子重复,导致生成的随机数高度重复,随机性极差;最优写法是外部创建对象,循环内复用调用方法。

Q3:Random和ThreadLocalRandom区别?

A:Random线程不安全,高并发竞争严重、性能差;ThreadLocalRandom是线程安全的并发随机工具类,每个线程独享随机种子,无锁竞争、并发性能极高,是企业高并发场景首选。

Q4:Math.random()底层原理?

A:底层本质是创建Random对象调用nextDouble()方法,仅能生成0~1随机小数,功能单一,适合简单场景,复杂随机需求建议直接使用Random。

三、System 系统工具类(系统级核心·实操+面试完整版)

1. 类核心特性(必背)

  • 所属包:java.lang,默认无需手动导包,直接使用

  • 构造方法:私有私有化 ,禁止实例化,所有属性、方法均为 static 静态

  • 核心定位:JVM系统级工具类,负责操作系统资源、JVM运行状态、程序终止、数组拷贝、系统参数获取等底层能力

  • 底层特点:大量方法为 native 本地方法,由C/C++实现,执行效率极高,直接操作底层系统资源

  • 全局特性:工具类全局唯一,所有操作直接作用于当前JVM进程

2. 三大核心全局常量(系统流对象)

System类内置三个全局静态流对象,贯穿所有Java项目日志、输入输出场景

  • System.out:标准输出流,默认指向控制台,用于打印输出日志、信息(日常最常用)

  • System.in:标准输入流,默认指向控制台,用于读取键盘输入数据

  • System.err:标准错误输出流,默认控制台红字输出,专门打印异常、错误信息,优先级高于out

java 复制代码
System.out.println("普通日志输出");
System.err.println("错误信息输出");

3. 全套高频核心方法(含实操代码案例)

(1)获取系统时间戳 currentTimeMillis()

  • 作用:获取当前系统时间与 1970-01-01 00:00:00 UTC 的毫秒级时间差

  • 返回值:long类型毫秒值,常用于统计代码执行耗时、生成时间戳唯一ID

  • 实操案例:

java 复制代码
// 获取当前时间戳
long time = System.currentTimeMillis();
// 统计代码执行耗时
long start = System.currentTimeMillis();
// 执行业务代码
Thread.sleep(100);
long end = System.currentTimeMillis();
System.out.println("执行耗时:" + (end - start) + "ms");

(2)程序终止 exit(int status)

  • 作用:直接终止当前整个JVM进程,彻底退出程序,优先级高于所有代码

  • 参数规则: status = 0:正常退出程序,无异常、无报错

  • status ≠ 0:异常退出程序,标识程序非正常终止

开发规范:禁止随意使用,仅在程序严重异常、强制终止场景使用,普通业务返回用return

案例: System.exit(0); // 正常退出JVM

(3)高效数组拷贝 arraycopy()(底层native·性能天花板)

  • 方法原型:public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

  • 核心作用:底层C语言实现的数组拷贝,Java数组拷贝效率最高的方式,远超循环赋值

  • 参数释义: src:源数组、srcPos:源数组起始拷贝下标

  • dest:目标数组、destPos:目标数组起始覆盖下标

  • length:拷贝元素个数

实操案例:

java 复制代码
int[] srcArr = {1,2,3,4,5};

int[] destArr = new int[5]; // 从源数组0下标开始,拷贝3个元素到目标数组0下标位置

System.arraycopy(srcArr,0,destArr,0,3); // 结果:

destArr = [1,2,3,0,0]

(4)系统属性获取 getProperty()

  • 作用:获取JVM、系统内置配置参数,适配跨系统兼容、环境判断场景

  • 高频常用参数:

java 复制代码
System.out.println(System.getProperty("os.name")); // 系统名称 

System.out.println(System.getProperty("java.version")); // JDK版本 

System.out.println(System.getProperty("user.home")); // 用户根目录
 
System.out.println(System.getProperty("file.separator"));// 文件分隔符(跨系统适配)

(5)垃圾回收触发 gc()

  • 方法:System.gc()

  • 作用:主动向JVM发送垃圾回收建议,提醒JVM执行GC

  • 核心注意:只是建议,不保证立即执行,JVM自主决定是否回收,无法强制GC

  • 开发规范:业务代码禁止频繁调用,影响程序性能,仅特殊内存释放场景酌情使用

4. 高频易错坑点(笔试+实操必避)

坑1:System.exit() 等同于 return 纠正:return仅结束当前方法,System.exit()直接终止整个JVM进程,所有线程、程序全部停止,严禁滥用。

坑2:System.gc() 可以强制垃圾回收 纠正:gc()只是给JVM的回收建议,JVM拥有最终决定权,无法强制、无法保证立即执行。

坑3:arraycopy() 会创建新数组 纠正:arraycopy是数组内容覆盖拷贝,目标数组必须提前初始化开辟空间,不会自动创建数组。

坑4:混淆out和err输出流 纠正:err输出优先级更高,控制台红字展示,多用于错误日志;out用于普通日志,二者输出顺序可能错乱。

5. 企业开发落地规范

  • 数组拷贝优先使用 System.arraycopy(),性能远优于for循环拷贝、Arrays普通拷贝

  • 代码耗时统计、时间戳记录,统一使用currentTimeMillis()

  • 业务流程终止优先用return、break,禁止随意使用System.exit()

  • 跨系统文件路径拼接,优先读取系统默认分隔符,适配Windows/Linux系统

  • 常规业务禁止手动调用System.gc(),交由JVM自动内存管理

6. 面试满分真题问答(直接背诵)

Q1:System.arraycopy() 为什么是最高效的数组拷贝方式?

A:该方法是native本地方法,底层由C/C++实现,直接操作内存地址进行批量拷贝,无需Java循环遍历,减少循环判断、寻址开销,是Java原生最高效的数组拷贝方案。

Q2:System.gc() 能否强制JVM垃圾回收?为什么?

A:不能。System.gc()仅向JVM发送垃圾回收请求、给出回收建议,JVM会根据当前内存占用、运行状态自主判断是否执行GC,开发者无法强制干预垃圾回收时机。

Q3:return 和 System.exit(0) 核心区别?

A:return仅终止当前方法,返回方法调用处,程序继续运行;System.exit(0)直接关闭终止整个JVM进程,所有线程、任务全部终止,程序彻底退出。

Q4:System类为什么不能实例化?

A:System类构造方法私有,所有功能均为静态静态属性和静态native方法,依托JVM底层实现,无需创建对象,直接类名调用即可,节省内存开销。

四、Runtime 运行时工具类(JVM进程专属·面试+实操完整版)

1. 类核心特性(必背)

  • 所属包:java.lang,默认无需手动导包,直接使用

  • 设计模式:饿汉式单例模式,全局唯一Runtime实例,禁止手动new创建对象

  • 核心定位:专门用于对接JVM底层运行时环境,获取JVM内存状态、进程信息、执行系统命令、操控虚拟机生命周期

  • 实例获取方式:唯一入口 Runtime.getRuntime(),全局共享同一个实例对象

  • 底层特点:大量方法依托JVM底层实现,可直接操作系统层级资源,是Java打通系统与虚拟机的核心工具类

2. 单例底层原理(面试高频)

  • 构造方法被private私有修饰,杜绝外部new实例化,保证全局唯一性

  • 类内部静态初始化唯一Runtime实例,JVM加载类时直接创建,全局常驻内存

  • 通过静态方法 getRuntime() 对外暴露唯一实例,全程单例复用,无重复对象开销

// 唯一获取实例方式,全局单例

Runtime runtime = Runtime.getRuntime();

3. 全套高频核心方法(含实操代码案例)

(1)JVM内存状态监控方法(性能排查必备)

  • totalMemory() :获取JVM当前已分配的总堆内存(包含已使用+未使用空闲内存),单位字节

  • freeMemory() :获取JVM当前空闲剩余内存,单位字节

  • maxMemory() :获取JVM最大可使用堆内存(虚拟机可扩容的内存上限),超出会触发OOM内存溢出

  • 实操案例:实时监控JVM内存占用

java 复制代码
Runtime runtime = Runtime.getRuntime();
// 总内存
long total = runtime.totalMemory();
// 空闲内存
long free = runtime.freeMemory();
// 最大内存
long max = runtime.maxMemory();
// 已使用内存 = 总内存 - 空闲内存
long used = total - free;

System.out.println("JVM总内存:" + total / 1024 / 1024 + " MB");
System.out.println("JVM空闲内存:" + free / 1024 / 1024 + " MB");
System.out.println("JVM最大内存:" + max / 1024 / 1024 + " MB");
System.out.println("JVM已用内存:" + used / 1024 / 1024 + " MB");

(2)系统命令执行 exec()(实操进阶)

  • 方法作用:调用本地系统终端命令,执行Windows/Linux系统指令,实现Java操控系统层级操作

  • 返回值:返回Process进程对象,可管控子进程、获取命令执行结果、销毁进程

  • 适配系统:自动适配操作系统,Windows执行cmd命令,Linux执行shell命令

  • 实操案例:执行系统命令、获取进程结果

java 复制代码
Runtime runtime = Runtime.getRuntime();
// Windows系统执行查看目录命令
Process process = runtime.exec("dir");
// Linux系统执行命令:runtime.exec("ls -l");

// 获取命令执行输出结果
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}
// 等待进程执行完毕,释放资源
process.waitFor();
// 销毁子进程
process.destroy();

(3)手动垃圾回收触发 gc()

  • 方法:runtime.gc()

  • 作用:向JVM发送垃圾回收建议,与System.gc()底层完全一致

  • 底层关联:System.gc() 底层本质就是调用 Runtime.getRuntime().gc()

  • 核心注意:仅为建议,不强制GC执行,JVM自主决策回收时机

(4)虚拟机退出/终止方法

  • exit(int status):终止JVM进程,与System.exit()完全一致,System.exit()底层调用该方法

  • halt(int status) :强制终止JVM,不执行钩子函数、不收尾资源,暴力退出,极少使用

(5)CPU核心数获取 availableProcessors()

  • 作用:获取当前服务器/电脑的可用CPU核心数

  • 开发场景:线程池核心线程数配置、并发任务适配CPU资源,是线程池优化核心依据

4. 高频易错坑点(笔试+实操必避)

坑1:Runtime可以new实例化

纠正:构造方法私有,禁止手动new,只能通过getRuntime()获取全局单例

坑2:exec()执行命令无结果、乱码报错

纠正:未指定字符集、未读取进程输入流、未等待进程结束,会导致命令执行不完整、乱码、进程残留

坑3:混淆exit()和halt()

纠正:exit()正常退出,执行收尾钩子方法;halt()暴力终止,不做任何资源收尾,易导致资源泄漏

坑4:频繁调用gc()优化内存

纠正:手动GC会打乱JVM自动内存管理机制,频繁调用会降低程序性能,业务禁止手动调用

5. 企业开发落地规范

  • 性能监控场景:使用内存、CPU核心数方法,用于服务监控、性能调优、线程池参数适配

  • 系统操作场景:exec()仅用于特殊业务执行系统命令,需做好权限校验、流关闭、进程销毁,避免资源泄漏

  • 资源管控场景:禁止滥用exit()、halt()终止JVM,业务退出优先使用return、异常捕获

  • GC优化规范:全程交由JVM自动GC,业务代码严禁手动调用runtime.gc()

6. 面试满分真题问答(直接背诵)

Q1:Runtime类为什么是单例?如何获取实例?

A:Runtime构造方法私有,采用饿汉式单例设计,保证全局只有一个JVM运行时实例,避免多次创建导致系统资源冲突;唯一获取方式是调用 Runtime.getRuntime()

Q2:System.gc() 和 Runtime.gc() 是什么关系?

A:二者完全等价,System.gc()底层源码直接调用 Runtime.getRuntime().gc(),都是向JVM发送垃圾回收建议,不强制回收。

Q3:Runtime.exec() 使用有哪些注意事项?

A:1. 需根据操作系统适配对应命令;

  1. 必须读取进程输入/错误流,避免进程阻塞;

  2. 执行完毕需等待进程结束并销毁子进程,防止资源泄漏;

  3. 手动指定字符集,规避乱码问题。

Q4:线程池配置为什么需要获取CPU核心数?

A:根据CPU核心数适配核心线程数,CPU密集型任务核心线程数配置为CPU核心数±1,可最大化利用CPU资源,避免线程过多导致的上下文切换开销,提升并发性能。

Q5:exit() 和 halt() 核心区别?

A:exit()是优雅退出,会执行JVM钩子函数、完成资源收尾、正常释放进程资源;halt()是暴力强制退出,不执行任何收尾逻辑,直接终止JVM,容易造成资源残留、数据异常。

4. 日期时间类(旧版+JDK8新版·面试重点)

Java日期时间类分为传统旧版(线程不安全)JDK8新版(线程安全、推荐),企业开发统一使用JDK8新时间API,旧版仅需了解兼容场景。

一、旧版日期类(了解即可,避坑重点)

Java 8之前的传统日期时间API,核心包含 java.util.Datejava.text.SimpleDateFormatjava.util.Calendar 三大类,整体设计繁琐、存在线程安全漏洞、API语义混乱、月份年份偏移问题严重,是开发高频坑点,目前仅用于老旧项目兼容,新项目一律禁止使用。

1. Date 日期时间核心类(老牌基础类)

(1)核心特性

  • 所属包:java.util.Date,需手动导包使用

  • 底层原理:内部封装一个long类型毫秒值,存储从【1970-01-01 00:00:00 UTC】到当前时间的毫秒时间差

  • 时间精度:精准到毫秒,是旧版时间体系的核心存储载体

  • 类特性:可变类,提供setTime()等修改方法,可篡改原有时间对象

(2)常用构造方法

  • new Date():空参构造,直接获取当前系统的默认时间

  • new Date(long date):传入毫秒值,初始化指定时间戳对应的日期对象

(3)核心常用方法

  • getTime():获取当前日期对象对应的毫秒时间戳

  • setTime(long time):修改当前日期对象的时间戳(破坏不可变性,线程不安全根源之一)

  • after(Date when):判断当前时间是否在指定时间之后

  • before(Date when):判断当前时间是否在指定时间之前

(4)经典代码演示

java 复制代码
// 1. 获取当前系统时间
Date now = new Date();
System.out.println("当前时间:" + now);

// 2. 获取时间戳
long timeStamp = now.getTime();
System.out.println("当前时间戳:" + timeStamp);

// 3. 根据时间戳创建指定时间
Date specifyDate = new Date(1720000000000L);
System.out.println("指定时间:" + specifyDate);
2. SimpleDateFormat 日期格式化类(高频坑点)

(1)核心作用 :实现 Date对象 ↔ 时间字符串 的双向转换,自定义时间展示格式,是旧版日期体系唯一的格式化工具。

(2)常用时间格式模板(开发必记)

  • yyyy-MM-dd HH:mm:ss:标准年月日时分秒(24小时制,最常用)

  • yyyy-MM-dd hh:mm:ss:12小时制年月日时分秒

  • yyyy/MM/dd:斜杠分隔日期格式

  • yyyyMMddHHmmss:无分隔紧凑时间格式(用于订单号、日志编号)

(3)核心方法与代码案例

java 复制代码
// 1. 创建格式化对象,指定标准时间格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 2. Date转字符串(格式化)
Date now = new Date();
String dateStr = sdf.format(now);
System.out.println("格式化时间:" + dateStr);

// 3. 字符串转Date(解析)
String timeText = "2025-08-07 14:30:00";
Date parseDate = sdf.parse(timeText);
System.out.println("字符串解析为日期:" + parseDate);
3. Calendar 日历工具类(废弃淘汰)

(1)核心定位 :JDK1.2推出,用于替代Date的部分过时方法,专门用于时间字段加减、年月日时分秒单独获取修改,是旧版时间操作工具类。

(2)核心特性

  • 抽象类,无法直接new,通过 Calendar.getInstance() 获取全局实例

  • 可单独获取/修改年、月、日、时、分、秒、星期等时间字段

  • 存在严重设计缺陷:月份从0开始(0=1月,11=12月)、年份偏移、API晦涩难懂

(3)简单实操案例

java 复制代码
// 获取日历实例
Calendar calendar = Calendar.getInstance();

// 获取当前年、月、日(月份需+1矫正)
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);

System.out.println(year + "年" + month + "月" + day + "日");

// 时间加减:往后加3天
calendar.add(Calendar.DAY_OF_MONTH, 3);
4. 旧版日期类四大致命缺陷(避坑核心·面试必考)
  • 线程极度不安全(最核心问题):SimpleDateFormat、Calendar为可变类,全局共享静态对象时,多线程并发格式化/解析时间,会出现时间错乱、数值覆盖、抛出异常,是线上高频BUG来源

  • API设计晦涩、容错率低:Calendar月份从0开始、年份存在偏移,极易出现手动计算错误;Date大量方法被废弃,新旧API混杂

  • 无时区、本地化适配:默认依赖系统本地时区,跨服务器、跨地区部署易出现时间偏差,无统一时区管理机制

  • 对象可变、数据不安全:Date、Calendar支持set修改时间,赋值传递后会篡改原对象数据,引发隐性业务BUG

5. 企业开发强制规范与替代方案
  • 禁止场景:新项目、并发接口、全局静态变量、时间格式化场景,一律禁止使用旧版API

  • 兼容场景 :老旧项目维护必须使用时,禁止定义全局static SimpleDateFormat,需方法内局部创建或使用ThreadLocal包装保证线程安全

  • 最优替代:全线替换为JDK8+ LocalDateTime、LocalDate、DateTimeFormatter线程安全时间API

6. 高频面试真题(旧版专属)

Q1:为什么SimpleDateFormat不适合做全局静态变量?

A:SimpleDateFormat是可变类,内部持有时间缓存字段,多线程并发调用format/parse方法时,会出现线程间数据覆盖,导致时间解析错乱、抛出转换异常,线程极度不安全。

Q2:Calendar类最大的设计坑点是什么?

A:月份取值范围是0~11,0代表1月、11代表12月,使用时必须手动+1矫正,极易出现业务时间统计错误,API设计不符合日常认知。

Q3:旧版日期API和JDK8新版API核心差异?

A:旧版可变、线程不安全、API繁琐、存在数值偏移、时区适配差;新版时间类不可变、线程安全、API简洁直观、无偏移BUG、支持精准时区控制,完全适配并发开发场景。

二、JDK8 全新时间API(企业首选·重点掌握)

JDK8 重磅推出全新日期时间API,位于java.time包下,彻底根治旧版Date、Calendar、SimpleDateFormat的所有痛点。

核心优势:所有时间类不可变、天然线程安全、API简洁直观、无时间偏移BUG、支持精准时区与时间戳转换,是目前企业开发、微服务项目、并发场景的唯一规范用法,旧版API全面淘汰。

全新时间体系核心七大核心类,全覆盖日常开发所有时间场景:

  • LocalDate:纯日期类(年月日),无时分秒,适用于生日、订单日期、归档日期

  • LocalTime:纯时间类(时分秒毫秒),无年月日,适用于每日定时、时段统计

  • LocalDateTime :完整日期时间类(年月日时分秒),开发最常用,全覆盖常规时间业务

  • DateTimeFormatter:线程安全时间格式化工具,替代不安全的SimpleDateFormat

  • Instant:时间戳类(UTC标准时间),精准到纳秒,底层时间存储核心

  • Duration:时间间隔类(时分秒毫秒间隔),用于计算两个时间的差值

  • Period:日期间隔类(年月日间隔),用于计算两个日期的差值

核心通用特性(必背) :所有时间类均被final修饰、不可变,所有时间修改、增减操作均返回新对象,原对象不改变,天然适配多线程并发场景,无任何线程安全问题。

1. 三大核心时间实体类精讲(LocalDate/LocalTime/LocalDateTime)

三类API方法规则完全统一,上手零成本,核心方法分为:获取当前时间、指定时间、时间增减、时间修改、时间对比五大类。

(1)LocalDate 纯日期操作(年月日)

java 复制代码
// 1. 获取当前系统本地日期
LocalDate nowDate = LocalDate.now();
System.out.println("当前日期:" + nowDate); // 格式:2025-08-07

// 2. 自定义指定日期(年、月、日)
LocalDate specifyDate = LocalDate.of(2025, 12, 31);
System.out.println("指定日期:" + specifyDate);

// 3. 单独获取年月日、星期
int year = nowDate.getYear(); // 年
int month = nowDate.getMonthValue(); // 月份(1-12,无偏移)
int day = nowDate.getDayOfMonth(); // 日
int week = nowDate.getDayOfWeek().getValue(); // 星期(1-7)

// 4. 日期增减运算
LocalDate nextDay = nowDate.plusDays(1); // 加1天
LocalDate lastMonth = nowDate.minusMonths(1); // 减1月
LocalDate nextYear = nowDate.plusYears(1); // 加1年

// 5. 日期对比
boolean isBefore = nowDate.isBefore(specifyDate); // 是否在指定日期之前
boolean isAfter = nowDate.isAfter(specifyDate); // 是否在指定日期之后
boolean isEqual = nowDate.isEqual(specifyDate); // 日期是否相等

(2)LocalTime 纯时间操作(时分秒)

java 复制代码
// 1. 获取当前系统本地时间
LocalTime nowTime = LocalTime.now();
System.out.println("当前时间:" + nowTime); // 格式:14:30:20.123

// 2. 自定义指定时间
LocalTime specifyTime = LocalTime.of(18, 30, 0); // 时分秒

// 3. 单独获取时分秒
int hour = nowTime.getHour();
int minute = nowTime.getMinute();
int second = nowTime.getSecond();

// 4. 时间增减运算
LocalTime nextHour = nowTime.plusHours(2); // 加2小时
LocalTime lastMinute = nowTime.minusMinutes(10); // 减10分钟

// 5. 时间对比
boolean timeBefore = nowTime.isBefore(specifyTime);

(3)LocalDateTime 完整日期时间(开发首选·全覆盖)

java 复制代码
// 1. 获取当前完整日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("当前完整时间:" + now);

// 2. 自定义指定日期时间
LocalDateTime specifyDateTime = LocalDateTime.of(2025, 8, 7, 14, 30, 0);

// 3. 时间增减(全覆盖年月日时分秒)
LocalDateTime tomorrowTime = now.plusDays(1).plusHours(3); // 加1天3小时
LocalDateTime lastWeekTime = now.minusWeeks(1); // 减1周

// 4. 精准修改时间
LocalDateTime changeDay = now.withDayOfMonth(15); // 修改为当月15号
LocalDateTime changeHour = now.withHour(20); // 修改小时为20点

// 5. 时间对比
boolean isFuture = now.isAfter(specifyDateTime);
2. DateTimeFormatter 线程安全格式化工具(核心替换)

核心优势:全局唯一、线程安全、可静态定义全局常量,彻底解决SimpleDateFormat并发报错问题,企业开发统一静态声明使用。

常用时间格式模板(必记)

  • yyyy-MM-dd HH:mm:ss :标准年月日时分秒(24小时制,通用)

  • yyyy-MM-dd :仅日期格式

  • HH:mm:ss :仅时间格式

  • yyyyMMddHHmmss :紧凑时间格式(订单号、流水号)

java 复制代码
// 1. 全局静态定义格式化器(推荐,全局复用)
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 2. 时间对象 → 字符串(格式化)
LocalDateTime now = LocalDateTime.now();
String timeStr = now.format(DATE_TIME_FORMATTER);
System.out.println("格式化时间:" + timeStr);

// 3. 字符串 → 时间对象(解析)
String parseStr = "2025-08-07 14:30:00";
LocalDateTime parseTime = LocalDateTime.parse(parseStr, DATE_TIME_FORMATTER);
System.out.println("解析后时间:" + parseTime);
3. Instant 时间戳类(底层存储·纳秒级精度)

Instant是JDK8新版时间戳核心类,基于UTC世界标准时间,精度达到纳秒级(旧版Date仅毫秒级),用于底层时间存储、时间戳转换、接口时间传输。

java 复制代码
// 1. 获取当前UTC时间戳
Instant instant = Instant.now();
System.out.println("UTC时间戳:" + instant);

// 2. 转换为毫秒时间戳(兼容旧版业务)
long milliTime = instant.toEpochMilli();

// 3. 毫秒时间戳转为Instant
Instant fromMilli = Instant.ofEpochMilli(System.currentTimeMillis());

// 4. Instant与LocalDateTime互转(需指定时区,解决8小时时差问题)
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());

关键坑点:Instant默认UTC时间,比北京时间慢8小时,转换本地时间必须绑定系统时区/Asia/Shanghai时区。

4. Duration & Period 时间间隔计算(实操高频)

专门用于计算两个时间/日期的差值,替代手动时间戳相减,代码更简洁、精准。

(1)Duration:计算时分秒毫秒间隔(适用于LocalTime/LocalDateTime)

java 复制代码
LocalDateTime start = LocalDateTime.of(2025, 8, 7, 10, 0, 0);
LocalDateTime end = LocalDateTime.of(2025, 8, 7, 14, 30, 0);

// 计算时间间隔
Duration duration = Duration.between(start, end);
System.out.println("间隔小时:" + duration.toHours()); // 4小时
System.out.println("间隔分钟:" + duration.toMinutes()); // 270分钟
System.out.println("间隔毫秒:" + duration.toMillis());

(2)Period:计算年月日间隔(适用于LocalDate)

java 复制代码
LocalDate birthDate = LocalDate.of(2000, 1, 1);
LocalDate nowDate = LocalDate.now();

// 计算日期间隔
Period period = Period.between(birthDate, nowDate);
System.out.println("间隔年:" + period.getYears());
System.out.println("间隔月:" + period.getMonths());
System.out.println("间隔日:" + period.getDays());
5. 新旧时间API互转(兼容老旧项目)

实际开发中需兼容旧版Date、Calendar,提供一键互转方案,适配老旧接口、数据库兼容场景。

java 复制代码
// 1. Date 转 LocalDateTime
Date oldDate = new Date();
LocalDateTime localDateTime = oldDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();

// 2. LocalDateTime 转 Date
Instant instant = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
Date newDate = Date.from(instant);

// 3. Calendar 转 LocalDateTime
Calendar calendar = Calendar.getInstance();
LocalDateTime calendarTime = calendar.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
6. 高频易错坑点(开发必避)

坑1:新版时间对象可变? 纠正:所有LocalDateTime/LocalDate/LocalTime均不可变,增减修改均返回新对象,必须接收返回值才生效。

坑2:Instant时间相差8小时 纠正:Instant默认UTC时区,转换本地时间必须绑定ZoneId.systemDefault()或上海时区。

坑3:DateTimeFormatter定义为局部变量 纠正:该类线程安全,建议定义为全局静态常量,减少对象创建开销。

坑4:Period/Duration正负值问题 纠正:between(开始时间, 结束时间),结束时间更早会返回负数,业务判断需处理正负。

坑5:混淆新旧时间格式化模板 纠正:新版模板规则与旧版一致,yyyy年、MM月、dd日、HH24小时、hh12小时,大小写不可错乱。

7. 企业开发强制规范(落地准则)
  • 新项目强制使用:所有时间业务统一使用JDK8+新时间API,禁止使用Date、SimpleDateFormat、Calendar。

  • 格式化统一规范:DateTimeFormatter全局静态常量定义,禁止方法内重复创建。

  • 时间存储规范:数据库datetime类型对应实体类LocalDateTime,date对应LocalDate,精准适配无偏移。

  • 并发场景首选:新版时间类天然线程安全,彻底杜绝时间错乱、解析异常等线上BUG。

  • 时间计算规范:时间差值计算优先使用Duration、Period,禁止手动时间戳相减,代码更优雅精准。

8. 面试满分真题问答(直接背诵)

Q1:JDK8新时间API相比旧版API的核心优势?

A:1. 线程安全:所有时间类、格式化类不可变,无并发安全问题;

  1. API简洁:无月份偏移、无需手动矫正,语义清晰;

  2. 功能更强:支持纳秒级精度、时区控制、精准时间间隔计算;

  3. 性能更优:不可变设计减少对象冗余,全局格式化器可复用。

Q2:DateTimeFormatter和SimpleDateFormat核心区别?

A:SimpleDateFormat是可变类,多线程并发会出现数据覆盖、解析异常,线程不安全;DateTimeFormatter为不可变类,天然线程安全,支持全局静态复用,性能更优,是企业开发唯一规范选择。

Q3:LocalDateTime为什么不需要处理线程安全问题?

A:LocalDateTime被final修饰,对象不可变,所有时间修改操作均生成新对象,原对象数据不会被篡改,多线程并发读写无数据竞争,天然线程安全。

Q4:Instant和LocalDateTime的区别?

A:Instant基于UTC标准时间,是底层时间戳存储载体,精度纳秒级;LocalDateTime基于本地时区,适配业务展示、时间计算,无时区偏移,二者需通过时区转换适配使用。

Q5:如何计算两个时间的间隔?新版API优势是什么?

A:时分秒间隔用Duration,年月日间隔用Period;相比传统时间戳相减,新版API无需手动换算单位,自带时分秒年月日差值统计,代码简洁、不易出错、可读性极强。

三、新旧版日期API核心区别(全维度精细化对比·面试+实操终版)

为彻底厘清Java新旧日期体系的核心差异,规避开发踩坑、应对面试深挖,下面从线程安全、对象特性、API设计、时间精度、时区适配、性能表现、报错坑点、企业适配场景八大核心维度,全方位细化对比,同时补充底层原理与落地区别。

1. 核心维度详细对比

(1)线程安全(最核心差异)

旧版API(Date/SimpleDateFormat/Calendar) :全部为可变类 ,内部持有可修改的时间缓存、字段属性。全局静态共享时,多线程并发格式化、解析、修改时间,会出现数据覆盖、时间错乱、解析异常、空指针报错,是线上高频BUG根源,绝对禁止全局复用

新版API(LocalDateTime系列/DateTimeFormatter) :所有时间类、格式化类均被final修饰,对象不可变,所有时间增减、修改、格式化操作均返回全新对象,原对象数据永久不变,天然适配多线程并发,可定义为全局静态常量复用,零并发风险。

(2)对象特性与数据安全性

旧版 :支持setTime()set()等修改方法,对象创建后可随意篡改时间数据。对象传递、赋值时会产生引用篡改问题,极易引发隐性业务BUG,数据安全性极差。

新版:无任何set修改方法,时间数据一旦初始化不可篡改,所有运算操作均生成新对象,数据只读安全,适配参数传递、缓存存储、集合存储等场景。

(3)API设计与使用体验

旧版:设计晦涩、不符合日常认知,存在大量偏移问题。Calendar月份取值0~11(0代表1月),必须手动+1矫正;年份、星期计算逻辑混乱;Date类大量方法被废弃,新旧API混杂,代码冗余易错。

新版:API简洁直观、语义清晰,月份1~12、星期1~7,无任何数值偏移;拆分纯日期(LocalDate)、纯时间(LocalTime)、完整时间(LocalDateTime),按需选用;提供专属时间加减、对比、间隔计算方法,无需手动运算,代码极简优雅。

(4)时间精度与存储能力

旧版 :仅支持毫秒级精度,底层基于long毫秒值存储,无法满足高精度时间记录、日志排序、接口耗时统计等精细场景。

新版 :支持纳秒级超高精度,Instant、LocalDateTime可精准记录纳秒时间,适配高并发服务、精密计时、分布式系统时间对齐场景。

(5)时区与国际化适配

旧版:时区能力薄弱,默认依赖系统本地时区,无统一时区管理机制。跨服务器、跨地区部署、国际化项目极易出现8小时时差、时间偏移问题,手动适配成本极高。

新版 :原生支持ZoneId时区类,可精准指定北京时间、UTC、海外时区,支持时区转换、时间校准,完美适配分布式项目、国际化系统、跨服务器部署场景,彻底解决时差问题。

(6)时间计算能力

旧版:无专属时间间隔工具,计算日期间隔、时长差,需手动获取时间戳相减,手动换算年月日时分秒,代码繁琐、易出错,不支持精准周期计算。

新版 :专属工具类支撑,Period 精准计算年月日间隔、Duration精准计算时分秒毫秒间隔,支持时间比对、周期加减、时间区间判断,全覆盖所有时间运算场景。

(7)性能与资源开销

旧版:SimpleDateFormat频繁创建销毁开销大,全局复用易并发报错;时间运算代码冗余、执行效率低,频繁修改会产生大量临时对象。

新版:不可变对象可全局复用,DateTimeFormatter静态常量无重复创建开销;底层算法优化,时间运算、格式化效率更高,GC压力更小,高并发场景性能碾压旧版。

(8)异常与容错机制

旧版:时间格式不匹配、数值越界时,报错模糊,难定位问题;空值、非法时间兼容差,极易抛出未知解析异常。

新版:异常信息精准清晰,明确提示格式错误、时间越界、时区异常;严格校验时间合法性,杜绝非法时间数据,业务容错性更强。

2. 核心底层本质差异(面试深挖)
  • 可变VS不可变 :旧版时间体系核心痛点是可变对象 ,所有并发问题、数据错乱均源于此;新版核心优势是不可变对象,从底层杜绝数据篡改风险。

  • 工具类设计差异:旧版格式化工具与时间对象绑定,状态可修改;新版格式化工具无状态、纯静态逻辑,全局无状态复用,符合工具类设计规范。

  • 体系完整性:旧版API碎片化,无统一时间体系;新版构建了「时间点(Instant)+日期时间(LocalXXX)+时区(ZoneId)+间隔(Period/Duration)+格式化(DateTimeFormatter)」完整闭环体系。

3. 企业开发适配规范(精准落地)
  • 全面淘汰旧版场景:所有新项目、微服务、并发接口、时间统计、格式化场景,一律禁止使用Date、Calendar、SimpleDateFormat。

  • 旧版兼容处理方案:老旧项目维护必须使用旧版API时,禁止定义全局静态SimpleDateFormat,可使用ThreadLocal包装保证线程安全,或局部创建用完即弃。

  • 新版强制落地规范:时间实体映射用LocalDate/LocalDateTime,时间格式化用全局静态DateTimeFormatter,时间差值计算用Period/Duration,高并发场景零风险。

4. 极简背诵口诀(面试秒答)

旧版可变不安全,精度低来时区烂,API晦涩坑点多,并发场景必报错;新版不变线程安,纳秒精度时区全,API简洁功能强,企业开发首选它。

5. 数组工具类 Arrays(实操高频·面试+落地完整版)

1. 类核心特性(必背)

  • 所属包:java.util.Arrays,需手动导包使用,是Java专属数组静态工具类

  • 类修饰:构造方法私有,禁止实例化 ,所有方法均为static 静态方法,直接通过类名调用

  • 核心定位:一站式解决数组的打印、排序、查找、拷贝、比较、填充、转集合等高频操作,简化原生数组繁琐代码,规避手写循环冗余逻辑

  • 适配类型:支持所有基本数据类型数组、引用类型数组,全覆盖Java所有数组场景

2. 全套高频核心方法(含完整实操代码)

(1)toString() 数组转字符串(解决地址值打印问题)

作用:将任意类型数组转为可读字符串格式,直接打印数组内容,替代原生数组输出地址值的问题,开发调试最常用。

方法重载:适配int、long、double、boolean、String、自定义对象等所有数组类型

java 复制代码
// 基本类型数组
int[] intArr = {1,3,5,7,9};
System.out.println(Arrays.toString(intArr)); // 输出:[1, 3, 5, 7, 9]

// 引用类型数组
String[] strArr = {"Java","MySQL","Spring"};
System.out.println(Arrays.toString(strArr)); // 输出:[Java, MySQL, Spring]
(2)sort() 数组排序(高频实操)

核心作用:对数组进行升序排序,底层根据数组类型适配不同排序算法,效率极高,无需手写排序逻辑。

底层排序原理(面试考点)

  • 基本类型数组:底层使用 快速排序,时间复杂度O(n log n),排序效率高、不稳定排序

  • 引用类型数组(String、对象):底层使用 归并排序,稳定排序,保证相等元素相对位置不变

方法细分与实操案例

java 复制代码
// 1. 全数组升序排序
int[] arr1 = {9,2,5,1,8};
Arrays.sort(arr1);
System.out.println(Arrays.toString(arr1)); // [1, 2, 5, 8, 9]

// 2. 区间排序(左闭右开:fromIndex~toIndex)
int[] arr2 = {9,2,5,1,8};
Arrays.sort(arr2,0,3); // 仅排序下标0-2元素
System.out.println(Arrays.toString(arr2)); // [2, 5, 9, 1, 8]

// 3. 引用类型数组排序(自然排序)
String[] strArr = {"banana","apple","orange"};
Arrays.sort(strArr);
System.out.println(Arrays.toString(strArr)); // [apple, banana, orange]
(3)binarySearch() 二分查找(高效查找前提:数组有序)

核心规则 :基于二分查找算法,必须先排序再查找,无序数组查找结果随机、不准确!

返回值规则

  • 查找成功:返回元素在数组中的对应下标

  • 查找失败:返回 -(插入点下标) - 1(负数标识未找到)

java 复制代码
int[] arr = {1,2,5,7,9};
// 有序数组查找
int index1 = Arrays.binarySearch(arr, 5);
System.out.println(index1); // 2(查找成功)

int index2 = Arrays.binarySearch(arr, 3);
System.out.println(index2); // -3(查找失败)

// 区间查找
int index3 = Arrays.binarySearch(arr,0,3,2);
System.out.println(index3); // 1
(4)copyOf() / copyOfRange() 数组拷贝与扩容

核心优势:底层基于System.arraycopy()实现,native方法效率极高,支持数组完整拷贝、区间拷贝、自动扩容缩容,是Java数组拷贝最优方案。

方法区别

  • copyOf(原数组, 新长度):从头拷贝,可实现数组扩容/缩容

  • copyOfRange(原数组, 起始下标, 结束下标):区间精准拷贝(左闭右开)

java 复制代码
int[] oldArr = {1,2,3,4,5};

// 1. 数组扩容:原数组5个元素,扩容为8位,空位补默认值0
int[] newArr1 = Arrays.copyOf(oldArr,8);
System.out.println(Arrays.toString(newArr1)); // [1, 2, 3, 4, 5, 0, 0, 0]

// 2. 数组缩容:截取前3个元素
int[] newArr2 = Arrays.copyOf(oldArr,3);
System.out.println(Arrays.toString(newArr2)); // [1, 2, 3]

// 3. 区间拷贝:下标1~4元素
int[] newArr3 = Arrays.copyOfRange(oldArr,1,4);
System.out.println(Arrays.toString(newArr3)); // [2, 3, 4]
(5)equals() 数组内容全等对比

核心作用 :对比两个数组内容、长度、元素顺序 是否完全一致,区别于数组 == 地址比较。

对比规则:长度一致、每个下标对应元素全部相等,才返回true,否则false。

java 复制代码
int[] arr1 = {1,2,3};
int[] arr2 = {1,2,3};
int[] arr3 = {1,3,2};

// 内容相等,返回true
System.out.println(Arrays.equals(arr1,arr2)); // true
// 元素顺序不同,返回false
System.out.println(Arrays.equals(arr1,arr3)); // false
// == 比较地址,两个不同对象地址不同
System.out.println(arr1 == arr2); // false
(6)fill() 数组批量填充赋值

核心作用:批量给数组全部/区间下标赋值,快速初始化数组,替代循环赋值,代码简洁高效。

java 复制代码
int[] arr = new int[5];

// 1. 全数组填充默认值
Arrays.fill(arr,66);
System.out.println(Arrays.toString(arr)); // [66, 66, 66, 66, 66]

// 2. 区间填充:下标1~3赋值88
Arrays.fill(arr,1,4,88);
System.out.println(Arrays.toString(arr)); // [66, 88, 88, 88, 66]
(7)asList() 数组转集合(高频坑点)

作用:将数组转为List集合,快速实现数组与集合的转换,适配集合工具类操作场景。

核心坑点(必考)

  • 仅支持引用类型数组(String、对象数组),基本类型数组转换会将整个数组作为单个集合元素

  • 返回的List是Arrays内部静态固定大小集合,不支持add()、remove()增删操作,仅支持查询、修改

  • 原数组修改会同步影响转换后的集合,二者底层共用同一数组数据

java 复制代码
// 正确用法:引用类型数组转集合
String[] strArr = {"A","B","C"};
List<String> list = Arrays.asList(strArr);
System.out.println(list); // [A, B, C]

// 错误用法:基本类型数组转集合(坑点)
int[] intArr = {1,2,3};
List<int[]> intList = Arrays.asList(intArr); // 集合元素为数组对象,非单个数值

3. 深层易错坑点(笔试高频避坑)

坑1:binarySearch无序数组查找

纠正:二分查找依赖有序数组,无序数组查找结果随机,业务使用必须先调用sort()排序。

坑2:混淆==与Arrays.equals()

纠正:==比较数组内存地址,判断是否为同一对象;Arrays.equals()比较数组内部内容,判断数值是否一致。

坑3:asList()转换集合随意增删

纠正:asList返回固定长度集合,底层无扩容逻辑,调用add/remove直接抛出UnsupportedOperationException异常。

坑4:基本类型数组通过asList转换数值集合

纠正:基本类型数组会被整体当作一个集合元素,需使用包装类数组(Integer\[\])才能正常转换数值集合。

坑5:拷贝后原数组修改不影响新数组

纠正:Arrays.copyOf属于浅拷贝,基本类型数组完全隔离;引用类型数组仅拷贝引用地址,修改新数组对象内容会影响原数组。

4. 浅拷贝&深拷贝核心区别(面试深挖)

  • 浅拷贝(Arrays.copyOf默认):仅拷贝数组内存地址,不拷贝数组内部对象,引用类型数组拷贝后新旧数组共享对象数据,修改相互影响,效率高、适用于基本类型数组。

  • 深拷贝:递归拷贝所有对象数据,新旧数组完全独立、互不影响,需手动遍历实现,适用于自定义对象数组、多维数组场景。

5. 企业开发落地规范

  • 数组打印、排序、查找、拷贝场景,禁止手写循环,统一使用Arrays工具类,代码简洁、性能更高。

  • 数组内容对比一律使用 Arrays.equals(),严禁使用==判断数组内容。

  • 数组转集合如需增删操作,需手动new ArrayList包装,规避固定集合报错问题。

  • 有序数组查找优先binarySearch二分查找,无序数组优先遍历查找,兼顾性能与准确性。

  • 批量初始化数组优先使用fill()方法,减少冗余循环代码,提升开发效率。

6. 面试满分真题问答(直接背诵)

Q1:Arrays.sort() 基本类型和引用类型排序算法有什么区别?

A:基本类型数组底层采用快速排序,排序速度快、非稳定排序;引用类型数组采用归并排序,稳定排序,保证相等元素相对位置不变,适配对象排序场景。

Q2:Arrays.equals() 和数组==的核心差异?

A:== 比较两个数组的内存地址,判断是否为同一个数组对象;Arrays.equals() 遍历对比数组长度、所有下标元素内容,判断数组数值是否完全一致,业务内容对比必须用equals方法。

Q3:Arrays.asList() 有哪些坑?如何解决?

A:1. 不支持基本类型数组转换数值集合;

  1. 返回固定大小集合,无法增删元素;

  2. 与原数组数据联动。

解决方案:使用包装类数组转换,手动new ArrayList接收集合,支持后续增删操作。

Q4:为什么二分查找必须保证数组有序?

A:二分查找通过中间值大小对比,折半缩小查找范围,依赖数组有序的特性;无序数组无法通过大小对比定位元素,查找结果完全失效。

Q5:Arrays.copyOf 是浅拷贝还是深拷贝?

A:默认浅拷贝。基本类型数组拷贝后数据完全独立;引用类型数组仅拷贝对象引用地址,新旧数组共享对象数据,修改会相互影响,复杂对象场景需手动实现深拷贝。

6. Objects 通用工具类(JDK7+ 空指针安全·企业规范终版)

1. 核心定位与设计初衷(必背)

Objects 是 java.util 包下JDK7新增的通用对象工具类 ,全程静态方法设计,专门用于解决Java开发中对象空指针异常、对象判空、等值比较、参数校验 等高频问题。弥补了传统对象操作必须手动判空、极易触发NPE(空指针异常)的短板,是目前企业开发强制规范使用的工具类,所有对象通用操作优先使用Objects,替代原生对象方法。

核心特性:工具类私有构造、无法实例化、所有方法static静态、天然空安全、适配所有引用类型对象。

2. 全部高频核心方法(底层原理+实操代码全覆盖)

(1)equals(Object a, Object b) 安全等值比较(开发最高频)

底层原理 :先做地址判等,再做非空校验,最后调用对象原生equals方法,自动规避空指针异常。完美解决原生obj.equals()当obj为null时报错的问题。

底层源码逻辑:a == b || (a != null && a.equals(b))

java 复制代码
// 传统写法:存在空指针风险
String str1 = null;
// str1.equals("test"); // 直接空指针报错!

// Objects安全写法:自动判空,零风险
boolean result1 = Objects.equals(str1, "test");
boolean result2 = Objects.equals(null, null); // true
boolean result3 = Objects.equals("Java", "Java"); // true
(2)isNull(Object obj) / nonNull(Object obj) 空值判断

方法作用:极简对象空值判断,语义清晰、代码优雅,替代传统 obj == null 判断,适配Lambda、Stream流式编程场景。

  • isNull(obj):判断对象是否为null,为空返回true,非空返回false

  • nonNull(obj):判断对象是否非空,非空返回true,为空返回false

java 复制代码
String name = null;
// 空值判断
boolean empty = Objects.isNull(name); // true
// 非空判断
boolean notEmpty = Objects.nonNull(name); // false

// 流式编程实战(过滤空对象)
List<String> list = Arrays.asList("Java", null, "Spring", null);
List<String> filterList = list.stream().filter(Objects::nonNull).collect(Collectors.toList());
(3)requireNonNull(T obj) 非空强制校验(参数校验核心)

核心作用 :强制校验对象非空,是接口参数、方法入参、实体校验 最优写法。对象为空直接抛出NullPointerException,非空则返回原对象,阻断非法空参数进入业务逻辑。

重载方法:支持自定义异常提示信息,适配业务精准报错

  • requireNonNull(T obj):默认空指针异常提示

  • requireNonNull(T obj, String message):自定义异常报错信息

java 复制代码
// 方法入参校验实战
public void getUserInfo(String userId){
    // 强制校验参数非空,为空直接抛出异常,阻断业务
    Objects.requireNonNull(userId, "用户ID不能为空!");
    // 后续正常业务逻辑
    System.out.println("查询用户:" + userId);
}
(4)hash(Object... values) 批量哈希值计算

作用 :批量传入多个参数,自动计算组合哈希值,重写hashCode()方法专用,替代手动拼接哈希逻辑,简洁且不易出错。

java 复制代码
// 实体类重写hashCode
@Override
public int hashCode() {
    // 批量组合多个字段生成哈希值
    return Objects.hash(id, name, age);
}
(5)toString(Object obj) 安全字符串转换

核心优势 :对象为null时不会报错,默认返回字符串 "null",非空则调用对象原生toString(),规避空指针。

java 复制代码
String str = null;
// 安全转换,不报错,输出null
String res = Objects.toString(str);
System.out.println(res);

3. 传统对象操作 VS Objects工具类(核心对比)

  • 传统obj.equals(target):调用者obj为null时,直接抛出空指针异常,必须手动前置判空,代码冗余

  • Objects.equals(obj,target):双向判空,无论哪个对象为null都不会报错,代码极简、安全

  • 传统obj == null:语法繁琐,语义不直观,流式编程适配性差

  • Objects.isNull():语义清晰、函数式编程适配、代码简洁规范

4. 高频易错坑点(开发必避)

坑1:Objects.equals能解决所有空指针问题

纠正:仅能规避比较阶段空指针,后续调用对象方法仍需判空,仅做等值安全对比。

坑2:isNull和nonNull混用逻辑混乱

纠正:nonNull等价于 !isNull,业务中优先用nonNull做非空过滤,代码可读性更高。

坑3:requireNonNull捕获异常使用

纠正:该方法用于参数拦截校验,无需try-catch,为空直接终止业务,符合快速失败原则。

坑4:Objects.hash单独使用单个字段

纠正:单字段哈希可直接调用字段hashCode,Objects.hash专为多字段组合哈希设计。

5. 企业开发强制落地规范

  • 对象等值比较 :一律使用 Objects.equals(),禁止原生对象.equals(),杜绝空指针隐患

  • 入参非空校验 :接口、公共方法入参统一使用 requireNonNull() 做前置校验

  • 空值判断场景:流式编程、集合过滤、对象判空优先使用 isNull/nonNull

  • 实体重写方法:重写hashCode优先使用Objects.hash批量计算,简洁不易出错

6. 面试满分真题问答(直接背诵)

Q1:Objects.equals() 与普通对象equals() 核心区别?

A:普通equals()方法要求调用对象非空,否则直接空指针报错;Objects.equals()底层实现双向判空逻辑,自动处理null场景,无论参数是否为空都不会报错,是线程安全、零NPE的规范写法,适配所有对象等值比较场景。

Q2:Objects.requireNonNull() 的使用场景和意义?

A:主要用于方法入参、接口参数、核心对象的前置非空校验,遵循快速失败原则,提前拦截空参数,避免空对象流入业务逻辑,引发后续隐性空指针异常、数据错乱问题,让报错精准定位、业务更健壮。

Q3:为什么推荐使用Objects工具类?

A:1. 天然空安全,彻底规避高频空指针异常;

  1. 代码简洁优雅,语义清晰可读性强;

  2. 适配函数式、流式编程,符合新版Java编码规范;

  3. 工具方法全覆盖对象判空、比较、校验、哈希场景,统一编码标准。

7. 基础类库高频面试真题(直接背诵·全覆盖终版)

Q1:String、StringBuilder、StringBuffer三者核心区别与选型原则?

A:1. 可变性:String底层final修饰,不可变;后两者是动态可变数组。

  1. 线程安全:String天然安全,StringBuilder无锁不安全,StringBuffer加synchronized锁安全。

  2. 性能:常量拼接String高效,单线程动态拼接StringBuilder最优,多线程共享拼接用StringBuffer。

  3. 选型口诀:固定不变用String,单拼多改用Builder,并发共享用Buffer。

Q2:String为什么设计成不可变?底层原理是什么?

A:设计目的:保障线程安全、支持常量池复用、稳定哈希值适配集合键、保证数据安全。

底层原理:String类被final修饰禁止继承,底层存储数组被private+final修饰,无对外修改方法,所有修改操作均生成新对象,原对象永久不可篡改。

Q3:String s = new String("abc") 会创建几个对象?为什么?

A:默认创建两个对象。

第一个:编译期将"abc"存入字符串常量池;

第二个:运行期通过new在堆内存创建全新String对象。只有new创建的对象不走常量池复用,会造成内存冗余,日常开发不推荐。

Q4:字符串拼接中,常量拼接和变量拼接底层有什么区别?

A:1. 纯常量拼接:编译期直接优化为最终字符串,走常量池,无性能损耗。

  1. 含变量拼接:运行期自动创建StringBuilder拼接,最终toString生成新堆对象,频繁循环拼接会产生大量GC垃圾,性能极差。

Q5:intern()方法的作用和底层机制?

A:主动将堆内存的String对象存入或复用字符串常量池。常量池存在对应内容则直接返回常量池地址;不存在则将当前堆对象存入常量池,实现对象复用,节约内存开销。

Q6:基本类型和包装类的核心区别?企业开发如何选型?

A:1. 默认值:基本类型有默认值,包装类默认null。

  1. 存储:基本类型存数值,包装类存对象地址。

  2. 适配场景:集合、泛型、数据库可空字段必须用包装类;方法局部变量、纯数值计算优先用基本类型,性能更高。

Q7:Integer缓存池的规则、范围及底层原理?

A:底层由IntegerCache静态内部类实现,JDK启动时预初始化对象。默认缓存范围-128~127,下限固定、上限可通过JVM参数修改。自动装箱优先走缓存,超出范围或new创建对象,均会新建堆对象,不走缓存。

Q8:自动装箱与拆箱的底层原理?会引发什么问题?

A:JDK5新增编译期特性:装箱是基本类型转包装类,底层调用valueOf()优先复用缓存;拆箱是包装类转基本类型,底层调用xxxValue()。

核心问题:null包装类拆箱会空指针,循环频繁装箱易产生大量临时对象,增加GC压力。

Q9:Float、Double为什么没有缓存?Boolean、Character缓存范围是什么?

A:Float、Double浮点数值无穷无尽,无法批量缓存;Boolean全局缓存true/false;Character缓存0~127基础ASCII字符,仅常用固定字符复用对象。

Q10:旧版Date时间API有哪些致命缺陷?

A:1. 线程不安全:SimpleDateFormat、Calendar为可变类,并发场景易时间错乱、报错。

  1. API晦涩:Calendar月份0~11需手动矫正,大量方法废弃。

  2. 精度低:仅支持毫秒级。

  3. 时区适配差,易出现8小时时差,对象可变易引发隐性BUG。

Q11:JDK8新版时间API核心优势及常用核心类?

A:优势:所有时间类不可变、天然线程安全,API简洁无偏移,支持纳秒精度、精准时区控制和时间间隔计算。

核心类:LocalDate(日期)、LocalTime(时间)、LocalDateTime(完整时间)、DateTimeFormatter(格式化)、Instant(时间戳)、Duration/Period(时间间隔)。

Q12:DateTimeFormatter和SimpleDateFormat核心区别?

A:SimpleDateFormat可变、线程不安全,全局共享会并发报错,只能局部创建;DateTimeFormatter不可变、天然线程安全,可定义为全局静态常量复用,性能更优,是企业开发唯一规范选择。

Q13:Duration和Period的使用区别?

A:Duration用于计算时分秒毫秒级时间间隔,适配LocalTime、LocalDateTime;Period用于计算年月日级日期间隔,仅适配LocalDate,二者精准适配不同时间差值场景,替代手动时间戳运算。

Q14:Arrays工具类常用方法及核心坑点?

A:常用方法:toString数组转字符串、sort排序、binarySearch二分查找、copyOf数组拷贝、equals内容对比、fill批量赋值、asList数组转集合。

核心坑点:binarySearch必须有序数组、asList返回固定大小集合不支持增删、仅引用类型数组可正常转集合。

Q15:Arrays.sort()基本类型和引用类型排序算法差异?

A:基本类型数组使用快速排序,速度快、非稳定排序;引用类型数组使用归并排序,稳定排序,保证相等元素相对位置不变,适配对象排序业务场景。

Q16:== 和 Arrays.equals()、Objects.equals()的三层区别?

A:1. ==:对比内存地址,判断是否为同一对象。

  1. Arrays.equals():对比数组长度和所有元素内容,判断数组内容一致。

  2. Objects.equals():双向判空安全对比,规避空指针,适用于所有对象等值判断。

Q17:Objects工具类核心方法及开发意义?

A:核心方法:equals安全判等、isNull/nonNull空值判断、requireNonNull参数非空校验、hash批量哈希计算、toString安全转字符串。

核心意义:规避空指针异常,统一代码规范,适配函数式编程,实现参数快速失败校验。

Q18:Objects.requireNonNull()的使用场景?

A:主要用于接口、公共方法入参、核心对象的前置非空校验,遵循快速失败原则,提前拦截空参数,避免空对象流入业务逻辑,引发隐性空指针、数据错乱问题,让报错精准可控。

Q19:StringBuilder为什么线程不安全?如何解决并发拼接问题?

A:StringBuilder所有方法无synchronized锁,多线程并发append会出现数据覆盖、计数错乱、内容缺失。解决方案:低并发可用StringBuffer,高并发场景优先ThreadLocal绑定StringBuilder,性能远高于StringBuffer。

Q20:为什么String适合做HashMap的Key?

A:String不可变,创建后哈希值永久固定,不会出现key哈希值变动导致的映射丢失、查找异常;同时支持字符串常量池复用,内存利用率高,适配集合键的使用要求。

相关推荐
青 春 记 忆11 小时前
零基础入门python30:Flask个人账本从空目录运行与阶段验收
python·flask·后端开发
l12586511 小时前
# LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践
前端·人工智能·python·langchain·bootstrap
丘山一郎12 小时前
Spring 中的IOC控制反转 和DI依赖注入
java·后端·spring
码视野12 小时前
基于 Spring Boot + Vue3 的【大学英语四六级 (CET-4/6) 作文智能评分与句式润色系统】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端·vue3
MetaLite12 小时前
Spring-AOP自调用为什么失效-AopContext真能解决吗
java·后端·spring
大模型码小白12 小时前
AI 对话流性能调优:万级消息的虚拟滚动落地
java·大数据·前端·javascript·人工智能·算法·机器学习
m0_5474866612 小时前
《JavaScript核心原理 》全套PPT课件2026
开发语言·javascript·ecmascript
许彰午12 小时前
14-字段级SM4加密与SM2签名
java·低代码·架构
MC皮蛋侠客12 小时前
Tauri 2.x 系列(五):调用系统 API——官方插件、Rust crate 与原生能力
开发语言·后端·rust
金銀銅鐵12 小时前
斐波那契数列的个位数出现的周期是多少?
python·数学