IntelliJ IDEA 常用快捷键(含效果演示) 以下按「编辑 → 导航 → 重构 → 调试 → 其它 」分组,每条都标注 效果展示 (改之前的代码 → 改之后的代码 / 菜单动作)。
默认以 Windows / Linux 键位为准;Mac 把 Ctrl 换成 ⌘、Alt 换成 ⌥。
一、编辑类(写代码最快)
快捷键
功能
效果展示
Ctrl + Alt + V
提取/生成局部变量(自动补左边)
new ArrayList<String>(); → List<String> list = new ArrayList<>();
Ctrl + Alt + F
提取为成员变量(Field)
方法里 user.getName() 多处用 → 提取 private String name = user.getName();
Ctrl + Alt + C
提取为常量(Constant)
if (status == 1) → if (status == ACTIVE)(顶部生成 private static final int ACTIVE = 1;)
Ctrl + Alt + M
提取为方法(Method)
选中一段重复逻辑 → 抽成 private void doXxx() 并自动替换调用处
Ctrl + Alt + t
选择代码生成try -catch
Ctrl + Alt + P
提取为方法参数(Parameter)
方法内用到局部变量 count → 提升为方法入参
Ctrl + D
复制当前行 / 选中块到下一行
int a = 1; ↓ → 多出新行 int a = 1;
Ctrl + Y
删除当前行
光标在 int b = 2; → 整行消失
Ctrl + X
剪切当前行(无需先选中)
光标在某行,直接剪走整行
Ctrl + /
行注释(//,可多行)
log.info("x"); → // log.info("x");
Ctrl + Shift + /
块注释(/* */)
选中多行 → 整体包裹 /* ... */
Alt + Enter
万能修复(意图动作)
红色报错处 → 自动 import、实现方法、类型转换、创建设置等
Ctrl + Shift + Enter
补全当前语句(补 ; 并换行)
list.add("a") → list.add("a"); 并跳到下一行
Ctrl + W
递进式选中(词→语句→块→方法)
多次按,选区从小到大向外扩
Ctrl + Shift + W
反向缩小选区
与 Ctrl + W 相反
Shift + Enter
在任意位置另起一行(不破坏当前行)
行中间按 → 光标直接到下行首
Ctrl + Shift + U
大小写切换
userName → USERNAME
Alt + J
多选相同词(逐个加选区)
多个 temp → 一次改全部
Ctrl + Alt + L
格式化代码(Reformat)
乱缩进代码 → 按规范对齐
Ctrl + Alt + O
优化 import(删无用、排顺序)
去掉未用的 import java.util.*
重点演示:Ctrl + Alt + V(自动生成左边变量)
1 2 3 4 5 6 7 new HashMap <String, List<Order>>();Map<String, List<Order>> map = new HashMap <>();
同类还有 Ctrl + Alt + N(内联,把变量/方法反过来还原进调用处),与”提取”互逆。
二、导航类(找代码最快)
快捷键
功能
效果展示
Ctrl + N
按类名搜索类
输入 ArrayL → 定位 ArrayList
Ctrl + Shift + N
按文件名搜索文件
输入 appli → 定位 application.yml
Ctrl + Shift + Alt + N
搜索符号(方法/字段名)
输入 getUs → 跳 getUser()
Ctrl + B / Ctrl + 单击
跳转到定义
光标在 userMapper → 跳到接口声明
Ctrl + Alt + B
跳到实现(接口→实现类)
接口方法 → 列出所有实现类任点
Ctrl + U
跳到父类 / 重写的方法
子类方法 → 跳父类定义
Alt + ← / →
左右切换已打开的文件标签页
在多个 tab 间快速移动
Ctrl + E
最近打开的文件列表
弹出近期文件,回车直达
Ctrl + Shift + E
最近编辑的位置列表
弹最近改动文件
Ctrl + [ / ]
移动光标到代码块的大括号首尾
函数内按 Ctrl + [ → 跳到 {
Ctrl + F12
查看当前类结构(方法/字段大纲)
弹窗列出全部成员,输入过滤
Shift + Shift(连按两次 Shift)
全局搜索(类/文件/动作/设置)
万能搜框,啥都能找
Ctrl + G
跳到指定行号
输入 120 → 跳 Test.java:120
F2
跳到下一个报错/警告处
逐个排查红线
三、重构 / 查找替换类
快捷键
功能
效果展示
Shift + F6
重命名(安全改名,引用全改)
改 getUser → 所有调用处同步改
Ctrl + R
当前文件替换
文件内 old → new
Ctrl + Shift + R
全局替换(跨文件)
整个项目 old → new
Ctrl + F
当前文件查找
文件内高亮 key
Ctrl + Shift + F
全局查找(跨文件)
全项目搜 TODO
F6
移动类 / 方法到别处
把工具方法移到 Util 类
Ctrl + F6
改方法签名(参数/返回类型)
加一个参数 → 调用处标红待补
四、调试类
快捷键
功能
效果展示
Shift + F9
Debug 模式启动
以调试态运行,可断点
Shift + F10
Run 普通运行
直接跑
F8
步过(Step Over,不进方法)
一行一行往下,方法当整体过
F7
步入(Step Into,进方法内部)
进入被调用方法体
Shift + F8
步出(Step Out,跳出当前方法)
从方法内回到调用处
Alt + F9
运行到光标处(Run to Cursor)
直接执行到光标那一行停下
F9
继续跑(Resume,到下一断点)
从当前断点放行
Ctrl + F8
加 / 取消断点
当前行红点切换
Alt + F8
调试中计算 / 表达式求值
弹窗输入 list.size() 看实时值
Ctrl + Shift + F8
查看 / 管理所有断点
列出全部断点
五、其它高频
快捷键
功能
效果展示
Ctrl + O
重写父类方法(Override)
弹父类方法列表勾选生成
Ctrl + I
实现接口方法(Implement)
弹接口方法列表勾选生成
Alt + Insert
生成代码(getter/setter/构造/toString)
类里按 → 勾 Getter and Setter 一键生成
Ctrl + P
查看方法参数提示
括号内按 → 显示 (String name, int age)
Ctrl + Q
查看光标处文档(Javadoc)
方法名上按 → 弹文档浮窗
Ctrl + Space
基础代码补全
输入 list. 后补成员
Ctrl + Shift + Space
智能类型补全
按返回值类型猜补全,更精准
Ctrl + Z / Ctrl + Shift + Z
撤销 / 重做
回退 / 前进操作
Alt + 1~9
打开对应侧边栏工具窗
Alt + 1 项目结构、Alt + 4 运行
💡 记不住就按 Shift + Shift 搜动作名(如搜 “extract variable” 也能触发 Ctrl + Alt + V);可在 Settings → Keymap 查看 / 自定义全部快捷键。
文档JDK https://itmyhome.com/java-api/
类型 基本数据类型(8种)
类型
字节
位数
取值范围
默认值
byte
1
8
-128 ~ 127
0
short
2
16
-32,768 ~ 32,767
0
int
4
32
-2^31 ~ 2^31-1(约±21亿)
0
long
8
64
-2^63 ~ 2^63-1
0L
float
4
32
±3.4E-38 ~ ±3.4E+38(6~7位有效数字)
0.0f
double
8
64
±1.7E-308 ~ ±1.7E+308(15~16位有效数字)
0.0d
char
2
16
0 ~ 65,535(Unicode字符)
‘\u0000’
boolean
-
1(逻辑)
true / false
false
常用引用类型
类型
说明
适用场景
BigInteger
任意精度整数,不可变
超出 long 范围的大整数运算
BigDecimal
任意精度小数,不可变
金融/货币等高精度计算,避免浮点误差
String
不可变字符序列
通用字符串
Integer/Long等
基本类型的包装类
泛型、集合、允许 null 的场景
类型转换
自动(隐式)转换 :小范围 → 大范围,无精度丢失byte → short → int → long → float → double
强制(显式)转换 :大范围 → 小范围,可能溢出或丢精度1 2 int i = 128 ;byte b = (byte ) i;
注意事项
int 字面量超出范围需加 L 后缀:long x = 2147483648L;
浮点数默认为 double,float 需加 f:float f = 3.14f;
BigDecimal 创建推荐用字符串构造:new BigDecimal("0.1") 而非 new BigDecimal(0.1)
整型包装类缓存:Integer 在 -128~127 内使用缓存,== 可能为 true
常见问题
== 运算符 基本数据类型 直接比较数值是否相等 引用数据类型:比较内存地址,即判断两个变量是否指向堆内存中的同一个对象。 equals() 方法 本质 :是 Object 类的方法,默认实现等同于 ==(比较地址)。
1 2 3 4 5 6 7 String s1 = "hello" ; String s2 = "hello" ; String s3 = new String ("hello" ); s1 == s2; s1 == s3; s1.equals(s3);
重写equals(), 当一个类中需要比较属性时,按 clt+ insert,可快速重写
String s = "abc";
StringBuilder d = new StringBuilder("abc");
s.equals(d) // false; s调用的equeal时是字符串,先判断是否是string再比较值,由于d不是string类型,所以返回 false
d.equeal(s) // false ; StringBuild没有equals方法,所以d的equeal使用的Object.equals. 也就是比较内存地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 > 💡 **前端视角(`==` 与 `equals`)**: > 这和 JavaScript 的"值比较 vs 引用比较"几乎一致。 > > - Java 基本类型 `==` 比**值**(如 `int a=1; int b=1; a==b` 为 true);引用类型 `==` 比**内存地址**(两个 `new` 出来的对象 `==` 几乎都是 false)——等价于 JS 里 `{} === {}` 为 false(两个对象引用不同)。 > - Java 的 `equals()`(正确重写后)比**内容**,≈ JS 里没有原生的"对象深比较",需要 `JSON.stringify(a) === JSON.stringify(b)` 或 lodash 的 `_.isEqual(a, b)`。 > - **关键差异**:Java 不同类型之间用 `==` 比较会**编译报错**(不会像 JS `==` 那样偷偷做类型转换)。所以 Java 的 `==` 更接近 JS 的严格相等 `===`,而 `equals()` 才是"看内容"。 ### 类 类只能单个继承,不能同时继续多个 ### 静态代码块(只执行一次) ```java static { // 静态代码块中的代码 System.out.println("静态代码块执行"); // 例如,初始化静态变量 staticVariable = "Initialized"; } static String staticVariable = "默认值"; public static void main(String[] args) { System.out.println(staticVariable); } }
封装方法防止被构造(设置private构造函数) 1 2 3 4 5 public class UtilityClass { private UtilityClass () { } }
抽象类的构造为了给子类初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.test;public abstract class Animal { private String name; private int age; public Animal () { } public Animal (String name, int age) { this .name = name; this .age = age; } public void Say (String msg) { System.out.println("animal类中的Say方法说:" + msg); } public abstract void toGo (String to) ; } package com.test;public class Dog extends Animal { public Dog (String name, int age) { super (name,age); } public Dog () { } @Override public void toGo (String msg) { System.out.println(this .getName() + "去了" + msg+",年龄为:" + this .getAge()); } public String toString () { return "Dog{}" ; } }
接口
类实现多个接口public class 类名 implements 接口1,接口2{}
类继承 and 实现接口public class 类名 extends parent implements 接口1,接口2{}
接口中的只能是常量,即使定义变量,继承类也修改不了(默认修饰public static final),公共的部分会抽离到父类抽象类中。
💡 运用场景:类 / 抽象类 / 接口 怎么选?
普通类 :最常用。有明确状态(字段)和行为(方法),且不需要被别人扩展时直接用类,如 User、OrderService。
抽象类 :当你有一组”共性逻辑 + 部分必须子类实现”时。例:Animal 里 Say() 公共逻辑写在父类,子类只实现 toGo();或模板方法模式(父类定流程骨架,子类填某一步)。抽象类能持有状态、有构造器 ,适合表达”is-a”关系(Dog 是 Animal)。
接口 :定义”能做什么”的能力契约,不关心谁来实现。例:Comparable(可比较)、Runnable(可运行)、Serializable。一个类可实现多个接口(弥补 Java 单继承)。JDK8+ 接口能写 default 方法 ,适合给老接口平滑加新方法而不破坏已有实现。
经验法则:描述”是什么/属于哪一类”用抽象类/继承;描述”具备什么能力”用接口;两者可组合,如 class Dog extends Animal implements Pet。
1 2 3 4 5 public interface IT { int a = 100 ; void method () ; }
接口继承 1 2 3 4 5 6 7 8 9 10 11 12 public interface IT1 { public abstract method1 () ; } public interface IT2 extends IT1 { public abstract method2 () ; } public class A implements IT2 { }
JDK8+接口新增方法
内部类 1 2 3 4 5 6 7 8 9 public class Car { public void show () { A ac= new A (); } class A { } }
内部类变量访问 1 2 3 4 5 6 7 8 9 10 11 12 13 class Outer { String name = "外部类变量" ; public class Inner { String name = "内部类变量" ; void method (String name) { System.out.println(name); System.out.println(this .name); System.out.println(Outer.this .name); } }} Outer out = new Outer ();Outer.Inner inner = new Outer ().new Inner ()
内部类中的静态类 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Outer { public final String name = "yuhongshao" ; public static class Inner { public String name = "inner name" ; public void log () { System.out.println(this .name); } } } Outer.Inner inn = new Outer .Inner()
匿名内部类 1 2 3 4 5 6 7 Api1 api1 = new Api1 (){ @Override public void log () { System.out.println("打印main" ); } }; }
https://blog.csdn.net/Aoutlaw/article/details/157699697
💡 运用场景:内部类到底什么时候用?
静态内部类(最常见) :做”配套类”,尤其 Builder 建造者模式 。例:User.Builder 链式构造对象;或把只服务于外部类的工具类收进去,避免污染包。
成员内部类 :需要访问外部类实例状态时使用(如迭代器 ArrayList$Itr 内部持有外部 list)。
匿名内部类(旧写法) :写一个”一次性的实现/回调”,比如老式 new Thread(() -> {...})、Swing 按钮点击事件、Comparator 排序。JDK8 之后大多被 Lambda 取代(只有需要多方法或访问外部非 final 变量等少数场景还用它)。
注意:普通内部类会隐式持有外部类引用,可能导致 内存泄漏 (Android 经典坑);不依赖外部实例时优先用 static 内部类。
Math
Math.abs(int x) x当超过int最大值时,返回负数, JDK15后增加了Math.absExact(int x) 用于校验x是否超过int最大值
System
System.currentTimeMillis():获取当前时间戳(自 1970-01-01 起的毫秒数)。用于计算程序运行耗时看性能。
System.nanoTime():纳秒级计时(仅用于测量间隔,绝对值无意义),精度更高。
System.getProperties() / System.getProperty(key):读取 JVM/系统属性(如 java.version、os.name)。
System.getenv(key):读取环境变量。
System.gc():建议 JVM 进行垃圾回收(不保证立即执行)。
System.exit(int status):status=0 表示正常退出,非 0 表示异常退出(终止整个 JVM)。
System.arraycopy 参数详解 (底层 native 实现,比手动 for 循环快很多)
1 public static void arraycopy (Object src, int srcPos, Object dest, int destPos, int length)
参数
含义
src
源数组(被复制的数组)
srcPos
源数组起始位置 (从哪个下标开始复制,含)
dest
目标数组(复制到的数组,必须与 src 类型兼容)
destPos
目标数组起始位置 (粘贴到哪个下标)
length
要复制的元素个数
1 2 3 int [] src = {1 , 2 , 3 , 4 , 5 };int [] dest = new int [5 ];System.arraycopy(src, 1 , dest, 0 , 3 );
引用数据类型时:子类数组可赋值给父类数组 (多态);复制的是引用(浅拷贝),改后对象属性,copy 后的对象属性也会跟着变 (两个数组元素指向同一对象)。 越界/类型不兼容会抛 IndexOutOfBoundsException / ArrayStoreException。
Runtime
Runtime.getRuntime() 获取Runtime对象
Runtime.getRuntime().exec(String command) 执行命令
Runtime.getRuntime().exit(int status) 退出程序
对象拷贝
💡 前端视角(拷贝) :
浅拷贝 ≈ JS 的对象展开 {...obj} 或 Object.assign({}, obj):只复制第一层属性,嵌套对象仍是同一个引用。改了拷贝出来的嵌套对象,原对象也会跟着变。
深拷贝 ≈ JS 的 JSON.parse(JSON.stringify(obj)):先把对象序列化成字符串、再解析回新对象,从而得到完全独立的一份。下面用 Gson 实现的 toJson → fromJson 就是 Java 版的 JSON.parse(JSON.stringify())。
浅拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class User implements Cloneable { @Override public Object Clone () { return super .clone() } } class Test { User u1= new User (); try { User u2 = (User) u1.clone(); System.out.println(u2.getAge()); }catch (CloneNotSupportedException e){ throw new RuntimeException (e); } }
需要在User类中实现重写Clone方法
需要在User类中实现Cloneable接口
拷贝时需要加try catch
写了implements Cloneable才能被拷贝,如果不写则不能拷贝
深拷贝
💡 前端视角(深拷贝 = JSON.parse(JSON.stringify())) :
1 2 const u2 = JSON .parse (JSON .stringify (u1));
Java 用 Gson 做完全一样的事:先 toJson 把对象序列化成 JSON 字符串,再 fromJson 解析成全新的对象实例。两者原理一致——靠”序列化→反序列化”断开引用链 。 ⚠️ 同样的坑也一致:函数/方法无法被序列化会被丢;Date/循环引用处理方式不同(Gson 能保留类型信息,而 JS 的 JSON.stringify 会把 Date 变成字符串、循环引用直接报错)。生产环境复杂对象建议用更专业的工具(如 Jackson、MapStruct)。
使用第三方工具gson-2.6.2.jar放/lib/gson-2.6.2.jar add as library解压
1 2 3 4 5 6 User u1 = new User ();Gson gson = new Gson ();String s = gson.toJson(u1); User u2 = gson.fromJson(s, User.class);
Objects
Objects.equals(obj1, obj2)
Objects.isNull(obj)
Object.nonNull(obj)
BigInteger 不可变的任意精度整数(java.math.BigInteger),用于超出 long(最大值 9223372036854775807,共 19 位)范围的大整数运算,如订单号生成、雪花ID、RSA 密钥、阶乘、高精度计数等。
为什么需要它:long 只有 64 位,最多约 19 位十进制;BigInteger 内部用 int[] 存储数值,理论上位数仅受 JVM 内存限制,可轻松处理上千位整数。
构造方法(参数详解)
构造方法
参数说明
异常/注意
BigInteger(String val)
val:十进制数字字符串,可带正负号 -,不能含小数点、不能含空白字符 (引号本身不是内容)
格式非法抛 NumberFormatException
BigInteger(String val, int radix)
val:按 radix 进制解析的字符串;radix:进制,取值范围 2~36 (2=二进制、8=八进制、10=十进制、16=十六进制)。超出范围抛异常
new BigInteger("FF",16) = 十进制的 255
BigInteger(byte[] val)
val:数值的二进制补码(大端序,最高位字节在前) 字节数组,首字节最高位为符号位
常与 toByteArray() 配对,用于序列化/网络传输
BigInteger(int numBits, Random rnd)
numBits:结果位长度(0 表示值 0);rnd:随机数发生器
返回 [0, 2^numBits - 1] 内均匀分布的随机数
BigInteger(int signum, byte[] magnitude)
signum:符号,**-1(负)/ 0(零)/ 1(正)**;magnitude:绝对值的大端补码数组
——
BigInteger(int bitLength, int certainty, Random rnd)
bitLength:位长度;certainty:素数确定性(越大越可靠);rnd:随机数
生成大概率素数,用于 RSA 密钥
1 2 3 4 5 6 7 8 BigInteger a = new BigInteger ("999999999999999999999" ); BigInteger b = new BigInteger ("111111111111111111111" );Random random = new Random ();BigInteger bi1 = new BigInteger (10 , random); System.out.println(bi1);
取值范围与类型转换
取值范围 :无固定上下限,仅受内存限制(实践可支持约 6.8 亿位十进制)。
转基本类型(溢出不报错,会静默截断 ,慎用):
intValue() / longValue() / doubleValue():溢出时返回截断值(intValue() 超出返回低位)
intValueExact() / longValueExact() / shortValueExact() / byteValueExact()(JDK8+):溢出时抛 ArithmeticException,推荐用于需要精确转换的场景
signum():返回 -1 / 0 / 1,比 compareTo(ZERO) 高效
toString(int radix):转指定进制的字符串,默认十进制
toByteArray():转补码字节数组
valueOf 静态工厂方法 vs 普通构造方法 BigInteger 同时提供静态工厂 BigInteger.valueOf(long) 与普通构造方法 new BigInteger(...),二者区别如下:
维度
BigInteger.valueOf(long)
new BigInteger(...)
入参类型
仅 long
String / byte[] / (int, Random) 等,功能更丰富
进制解析
不支持
支持,如 new BigInteger("FF", 16) = 255
对象复用
内部缓存 [-16, 16] 范围的值 ,重复调用返回同一实例
每次都新建对象
性能
对小整数更高效(避免重复分配)
略重
引用相等 ==
小范围内 valueOf(x) == valueOf(x) 可能为 true
永远 false
valueOf 内部维护 [-16, 16] 的小整数缓存(源码 MAX_CONSTANT = 16),该范围内取值直接复用同一对象;构造方法永远 new。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 BigInteger a = BigInteger.valueOf(16 );BigInteger b = BigInteger.valueOf(16 );System.out.println(a == b); System.out.println(a.equals(b)); BigInteger c = BigInteger.valueOf(17 );BigInteger d = BigInteger.valueOf(17 );System.out.println(c == d); BigInteger e = new BigInteger ("16" );BigInteger f = new BigInteger ("16" );System.out.println(e == f); System.out.println(e.equals(f));
重要原则 :无论用哪种方式创建,**比较值都必须用 .equals(),绝不能用 ==**(== 只判断引用,缓存仅覆盖 [-16, 16],边界外必然失效):
1 2 if (a.equals(b)) { ... } if (a == b) { ... }
选型建议 :手头是 long 数值 → 优先 BigInteger.valueOf(x),更轻量、可复用;需要字符串/进制解析或生成随机大数 → 用 new BigInteger(...)。
同类包装类的 valueOf 缓存对比 (静态工厂是 Java 包装类的通用范式):
类
valueOf 缓存范围
说明
Boolean
TRUE / FALSE
全部缓存
Byte
[-128, 127]
全部缓存
Short
[-128, 127]
部分缓存
Integer
[-128, 127](可调 -XX:AutoBoxCacheMax)
部分缓存
Long
[-128, 127]
部分缓存
Character
[0, 127]
部分缓存
Float / Double
无缓存
每次 valueOf 都新建
BigInteger
[-16, 16]
仅适用于 valueOf,不适用 new
常用方法(参数与返回值)
方法
参数
返回值 / 行为
注意
add(BigInteger val)
val:加数
this + val
不可变,返回新对象
subtract(BigInteger val)
val:减数
this - val
——
multiply(BigInteger val)
val:乘数
this * val
——
divide(BigInteger val)
val:除数(不可为 0 )
this / val,向零截断取整
val==0 抛 ArithmeticException
remainder(BigInteger val)
val:除数
余数(符号同 this)
与 % 一致
mod(BigInteger val)
val:模数(必须正 )
模(结果非负)
val<=0 抛 ArithmeticException
divideAndRemainder(BigInteger val)
val:除数
长度为 2 的数组 [商, 余数]
一次调用得商和余
pow(int exponent)
exponent:非负 int (0~`Integer.MAX_VALUE`)
this ^ exponent
负数指数抛 ArithmeticException
gcd(BigInteger val)
val:另一数
最大公约数
——
abs() / negate()
无
绝对值 / 相反数
——
max(BigInteger val) / min(BigInteger val)
val:比较对象
较大 / 较小值
——
compareTo(BigInteger val)
val:比较对象
this>val→1,相等→0,<→-1
比较大小必须用这个,不要用 equals
equals(Object obj)
obj:对象
值和精度都相等才 true
1 与 1 相等;不同对象类型为 false
shiftLeft(int n) / shiftRight(int n)
n:移位位数
乘/除 2^n
左移负数=右移
and/or/xor(BigInteger val) / not()
val:操作数
按位运算
——
modPow(BigInteger e, BigInteger m)
e:指数;m:模
(this^e) mod m
密码学(RSA)核心
isProbablePrime(int certainty)
certainty:确定性(10 基本可信,100 极高)
true=大概率素数
蒙特卡洛算法
nextProbablePrime()
无
大于 this 的下一个 probable prime
——
1 2 3 4 5 6 7 8 9 BigInteger sum = a.add(b); BigInteger diff = a.subtract(b); BigInteger prod = a.multiply(b); BigInteger quot = a.divide(b); BigInteger mod = a.mod(b); BigInteger[] qr = a.divideAndRemainder(b); int compare = a.compareTo(b); BigInteger pow = a.pow(3 ); boolean prime = a.isProbablePrime(10 );
常见业务案例:超大订单号 / 分布式ID 拼接
1 2 3 4 BigInteger orderId = new BigInteger (System.currentTimeMillis() + "" ) .multiply(BigInteger.valueOf(10000 )) .add(BigInteger.valueOf(seq));
BigDecimal 不可变的任意精度十进制数(java.math.BigDecimal),用于金融/货币/百分比 等要求精确计算的场景,避免 double/float 的二进制浮点误差。
核心概念:精度(precision) = 有效数字位数;标度(scale) = 小数点后位数。new BigDecimal("1.00") 的 scale=2,new BigDecimal("1") 的 scale=0,二者 equals 不相等,但 compareTo 相等。
构造方法(参数详解)
构造方法
参数说明
注意
BigDecimal(String val)
val:数字字符串(如 "10.05")
推荐 ,精确无误差
BigDecimal(double val)
val:double(如 10.05)
不推荐 ,0.1 实际为 0.1000000000000000055...
BigDecimal(int val) / BigDecimal(long val)
整数
精确
BigDecimal(BigInteger val)
整数 BigInteger
精确
BigDecimal(char[] in) / BigDecimal(char[] in, int offset, int len)
字符数组 / 子数组
解析字符为数值
BigDecimal(String val, MathContext mc)
val:字符串;mc:精度上下文(含精度与舍入)
构造时即按 mc 舍入
1 2 3 4 5 BigDecimal a = new BigDecimal ("10.05" );BigDecimal b = new BigDecimal ("3" );
RoundingMode 取整模式(重点) divide、setScale 等必须指定舍入模式,否则除不尽会抛 ArithmeticException。
模式
含义
示例 2.345→1位
RoundingMode.HALF_UP
四舍五入(金额最常用 )
2.3
RoundingMode.HALF_DOWN
五舍六入
2.3
RoundingMode.HALF_EVEN
银行家舍入(四舍六入五取偶,最小偏差)
2.4(2.35→2.4)
RoundingMode.UP
远离零方向(绝对值变大)
2.4
RoundingMode.DOWN
趋向零方向(截断)
2.3
RoundingMode.CEILING
趋向正无穷
2.4
RoundingMode.FLOOR
趋向负无穷
2.3
RoundingMode.UNNECESSARY
要求精确整除,否则抛异常
——
常用方法(参数与返回值)
方法
参数
返回值 / 行为
注意
add(BigDecimal augend)
augend:加数
this + augend
结果 scale = max(scale1, scale2)
subtract(BigDecimal subtrahend)
subtrahend:减数
this - subtrahend
——
multiply(BigDecimal multiplicand)
multiplicand:乘数
this * multiplicand
结果 scale = scale1 + scale2
divide(BigDecimal divisor)
divisor:除数
this / divisor
除不尽必抛 ArithmeticException
divide(BigDecimal divisor, int scale, RoundingMode rm)
divisor:除数;scale:保留小数位;rm:舍入模式
指定精度和舍入
最常用除法重载
divide(BigDecimal divisor, int scale, int rm)
同上,rm 用 int(已废弃常量)
同上
推荐用 RoundingMode 版本
divide(BigDecimal divisor, RoundingMode rm)
用 this 的 scale 舍入
除到 this 的标度
——
divide(BigDecimal divisor, MathContext mc)
mc:精度上下文
按 mc 精度舍入
——
remainder(BigDecimal divisor)
divisor:除数
余数
符号同 this
divideAndRemainder(BigDecimal divisor)
divisor:除数
[商, 余数] 数组
——
setScale(int newScale, RoundingMode rm)
newScale:新小数位;rm:舍入
设置/调整小数位数 (最常用做”保留 N 位”)
setScale(2, HALF_UP) 经典两位金额
setScale(int newScale)
newScale:新小数位
去除多余小数(要求可精确去除,否则抛异常)
仅保留位数,不四舍五入慎用
stripTrailingZeros()
无
去掉末尾无效 0(如 1.00→1)
配合 toPlainString() 防科学计数法
pow(int n)
n:非负指数
this^n
——
abs() / negate() / signum()
无
绝对值 / 相反数 / 符号
——
compareTo(BigDecimal val)
val:比较对象
1 / 0 / -1
比较大小用这个
equals(Object obj)
obj:对象
值和 scale 都相同 才 true
1.0.equals(1.00) = false
max/min(BigDecimal val)
val:比较对象
较大/较小
基于 compareTo
movePointLeft(int n) / movePointRight(int n)
n:移动位数
小数点左/右移 n 位
——
toPlainString() / toString()
无
转字符串
toString() 大数可能转科学计数法
1 2 3 4 5 BigDecimal quot = a.divide(b, 2 , RoundingMode.HALF_UP); a.compareTo(b);
常见案例:金额计算(价格 × 数量 × 折扣)
1 2 3 4 5 6 7 8 9 10 BigDecimal price = new BigDecimal ("19.99" );BigDecimal quantity = new BigDecimal ("3" );BigDecimal total = price.multiply(quantity); BigDecimal discount = total.multiply(new BigDecimal ("0.85" )) .setScale(2 , RoundingMode.HALF_UP); BigDecimal rate = new BigDecimal ("0.0365" );BigDecimal interest = new BigDecimal ("10000" ) .multiply(rate).setScale(2 , RoundingMode.HALF_UP);
💡 运用场景:任何”钱 / 比率”都别用 double
订单金额、账户余额、费率、税费、优惠券折扣 :这些差一分钱都是事故。永远用 BigDecimal,且用字符串构造 。
典型坑 :double a = 0.1 + 0.2; 结果是 0.30000000000000004(二进制浮点无法精确表示),直接用于金额就出错;而 new BigDecimal("0.1").add(new BigDecimal("0.2")) 精确等于 0.3。
数据库对应 :MySQL 金额字段用 DECIMAL(10,2)(不是 FLOAT/DOUBLE),Java 用 BigDecimal 接,端到端保证精度。
对外传输 :前后端通过 JSON 传金额,建议传字符串 ("19.99")而非数字,避免前端 JS 0.1+0.2 同样丢精度(前端同学尤其要注意)。
字符串处理
StringBuilder — 单线程频繁修改/拼接字符串(可变、非线程安全、性能好)
StringBuffer — 多线程安全版本的 StringBuilder(加了 synchronized)
StringJoiner — 按分隔符拼接字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 StringBuilder sb = new StringBuilder ();sb.append("Hello" ).append(" " ).append("World" ); sb.insert(5 , "," ); sb.reverse(); String result = sb.toString();StringJoiner sj = new StringJoiner (", " , "[" , "]" );sj.add("Java" ).add("Python" ).add("Go" ); System.out.println(sj); String joined = String.join("-" , "2026" , "07" , "09" );
常见案例:SQL 拼接
1 2 3 4 List<String> conditions = new ArrayList <>(); if (name != null ) conditions.add("name = '" + name + "'" );if (age > 0 ) conditions.add("age = " + age);String where = conditions.isEmpty() ? "" : " WHERE " + String.join(" AND " , conditions);
字符串常用方法速查 String 是不可变字符序列(每次”修改”都返回新对象)。下表按方法 → 参数 → 返回值 → 注意 说明:
方法
参数
返回值 / 行为
注意
length()
无
字符个数(≠字节数)
中文按 1 个字符计
charAt(int index)
index:下标(0 起,范围 0~length-1 )
第 index 个字符
越界抛 StringIndexOutOfBoundsException
substring(int begin)
begin:起始下标(含)
[begin, 末尾) 子串
——
substring(int begin, int end)
begin:起始(含);end:结束(不含 )
[begin, end) 子串
end 可到 length;s="Hello" 中 substring(0,5)=”Hello”
indexOf(String str)
str:子串
首次出现下标,无则 -1
indexOf("World")→6
indexOf(String str, int from)
str:子串;from:起始搜索位置
从 from 起首次出现下标
用于循环查找所有出现
lastIndexOf(String str)
str:子串
最后一次出现下标
——
contains(CharSequence s)
s:子序列
是否包含
等价于 indexOf(s)>=0
startsWith(String prefix)
prefix:前缀
是否以此开头
startsWith("He")→true
startsWith(String prefix, int toffset)
prefix:前缀;toffset:偏移起点
从 toffset 起是否以此前缀
——
endsWith(String suffix)
suffix:后缀
是否以此结尾
endsWith("ld")→true
trim()
无
去首尾 ASCII 空白(空格、Tab、换行)
不去全角空格
strip()(JDK11+)
无
去首尾所有 Unicode 空白(含全角空格)
推荐替代 trim
stripLeading() / stripTrailing()(JDK11+)
无
仅去前/后空白
——
replace(CharSequence a, CharSequence b)
a:被替换;b:替换成
全部字面替换(非正则 )
replace("World","Java")→”Hello Java”
replaceAll(String regex, String repl)
regex:正则 ;repl:替换(支持 $1 反向引用)
按正则全部替换
replaceAll("\\s+","-") 空格→-
replaceFirst(String regex, String repl)
同上,仅替换首个匹配
首个匹配替换
——
split(String regex)
regex:正则 分隔符
字符串数组
split(" ")→[“Hello”,”World”]
split(String regex, int limit)
regex:正则;limit:结果长度上限
同上,limit<=0 保留末尾空串,limit>0 最多 limit-1 次分割
split(",", -1) 保留空串
toUpperCase() / toLowerCase()
无
转大小写
注意 Locale,默认系统环境
toUpperCase(Locale) / toLowerCase(Locale)
Locale:区域
指定区域转大小写
土耳其语等特殊场景
isEmpty()
无
length()==0 时 true
"".isEmpty()→true
isBlank()(JDK11+)
无
空或仅空白字符 true
" ".isBlank()→true
concat(String str)
str:拼接串
等价于 + 拼接
不可变,返回新串
equals(Object) / equalsIgnoreCase(String)
比较对象 / 忽略大小写比较串
是否内容相等
比较内容用 equals,非 ==
compareTo(String)
other:另一串
字典序差(负数/0/正)
区分大小写
matches(String regex)
regex:正则
整体是否匹配
等价于 Pattern.matches
format(String fmt, Object... args)
fmt:格式串;args:参数(变长)
格式化字符串
见下方格式符表
valueOf(...)(静态)
任意基本类型/对象
转字符串
比 obj.toString() 更安全(null→”null”)
join(CharSequence d, Iterable/数组)(静态,JDK8+)
d:分隔符;元素集合
用分隔符拼接
String.join("-","a","b")→”a-b”
常用 format 格式符(String.format)
格式符
含义
示例
%s
字符串
"Hi %s".format(“Tom”) → “Hi Tom”
%d
十进制整数
"age %d".format(25) → “age 25”
%f
浮点数
"%.2f".format(3.14159) → “3.14”
%tF / %tT
日期/时间
配合 Date/LocalDateTime
%n
换行
跨平台换行
%x / %X
十六进制
——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 String s = "Hello World" ;s.length(); s.charAt(0 ); s.substring(0 , 5 ); s.indexOf("World" ); s.contains("llo" ); s.startsWith("He" ); s.endsWith("ld" ); s.trim(); s.strip(); s.replace("World" , "Java" ); s.replaceAll("\\s+" , "-" ); s.split(" " ); s.toUpperCase(); s.toLowerCase(); s.isEmpty(); s.isBlank(); String.format("Hi %s, age %d" , "Tom" , 25 );
常用工具类与核心类详解 Arrays(数组工具类) java.util.Arrays,数组操作的静态工具类。
方法
参数
返回值 / 行为
注意
sort(T[] a)
a:待排序数组(基本类型或对象)
原地升序排序
对象需实现 Comparable 或提供 Comparator
sort(T[] a, int from, int to)
from:起始(含);to:结束(不含 )
对 [from, to) 区间排序
越界抛 IllegalArgumentException
sort(T[] a, Comparator<? super T> c)
c:比较器
按比较器排序
Comparator.reverseOrder() 降序
parallelSort(T[] a)
a:数组
并行排序(多核)
大数组性能更好,小数组反而慢
binarySearch(T[] a, T key)
a:已排序数组;key:查找值
命中返回下标,未命中返回 -(插入点)-1
必须先排序 ,否则结果无意义
binarySearch(T[] a, int from, int to, T key)
区间 + key
区间二分查找
同上
copyOf(T[] original, int newLength)
original:原数组;newLength:新长度
新数组
不足补默认值(0/null),超出截断
copyOfRange(T[] original, int from, int to)
from:起(含);to:止(不含)
复制区间
to 可超出长度,超出补默认
fill(T[] a, T val)
a:数组;val:填充值
全部填 val
原地修改
fill(T[] a, int from, int to, T val)
区间 + 值
区间填充
——
equals(T[] a, T[] b)
两个数组
长度且逐元素相等 true
比较内容,非地址
deepEquals(Object[] a, Object[] b)
嵌套数组
递归深度比较
含二维数组时用
toString(T[] a)
数组
"[1, 3, 5]" 形式字符串
基本类型/对象数组通用
deepToString(Object[] a)
嵌套数组
递归转字符串
多维数组打印
asList(T... a)
变长参数/数组
固定大小的 List
不能 add/remove (UnsupportedOperationException)
setAll(T[] a, IntFunction<? extends T> gen)
生成函数
按索引生成元素
setAll(arr, i -> i*2)
stream(T[] a)
数组
转 Stream
基本类型用 IntStream 等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.util.Arrays;int [] arr = {5 , 3 , 8 , 1 , 9 };Arrays.sort(arr); Arrays.sort(arr, 0 , 3 ); int index = Arrays.binarySearch(arr, 5 ); int [] copy = Arrays.copyOf(arr, 10 ); int [] range = Arrays.copyOfRange(arr, 1 , 4 ); Arrays.fill(arr, 0 ); boolean eq = Arrays.equals(arr1, arr2); String str = Arrays.toString(arr); User[] users = ...; Arrays.sort(users, Comparator.comparingInt(User::getAge)); int sum = Arrays.stream(arr).sum();List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList()); Arrays.parallelSort(arr); import java.util.Arrays;int [] arr = new int [100 ];Arrays.fill(arr, 0 , 70 , 1 ); Arrays.fill(arr, 70 , 100 , 2 ); System.out.println(Arrays.toString(arr));
场景 :数据预处理、排序、查找、数组↔集合转换
常见案例:批量初始化区间值
1 2 int [] idx = new int [10 ];Arrays.setAll(idx, i -> i * 2 );
lambda 表达式的使用 当方法参数是函数式接口 (只有一个抽象方法的接口,如 Comparator、IntFunction)时,可用 lambda 简化:
1 2 3 4 5 6 7 8 Arrays.sort(users, (a, b) -> a.getAge() - b.getAge()); Arrays.setAll(arr, i -> i * 2 ); Arrays.sort(arr); Arrays.fill(arr, 0 ); int n = Arrays.binarySearch(arr, 5 );
什么时候能用 / 不能用
场景
能否用 lambda
说明
sort(数组, 比较器)、setAll、stream 的 filter/map 等
✅ 能用
参数是函数式接口
sort(数组)、fill、copyOf、toString、binarySearch 等
❌ 不能用
参数只是普通值或数组,不是接口
简单判断:看方法参数位置「是否要传一段逻辑(代码)」,是就能用 lambda,只是传数据就不能。
Collections(集合工具类) java.util.Collections,操作 Collection/List/Map 的静态算法与工厂方法。
方法
参数
返回值 / 行为
注意
sort(List<T> list)
list:待排序
原地升序
元素需 Comparable
sort(List<T> list, Comparator<? super T> c)
list:列表;c:比较器
按比较器排序
Comparator.reverseOrder() 降序
reverse(List<?> list)
list:列表
反转顺序
原地
shuffle(List<?> list)
list:列表
随机打乱
可传 Random 指定种子
swap(List<?> list, int i, int j)
i/j:两个下标
交换位置
越界抛 IndexOutOfBounds
rotate(List<?> list, int distance)
distance:移动距离
循环移动(正右移,负左移)
rotate(list,2) 右移2位
max(Collection) / min(Collection)
集合
最大 / 最小元素
需 Comparable
max(Collection, Comparator) / min(Collection, Comparator)
集合 + 比较器
按比较器取最大/小
——
frequency(Collection, Object o)
o:目标元素
出现次数(含 null)
frequency(list, 1)→次数
binarySearch(List, key)
已排序列表 + key
下标或 -(插入点)-1
必须先 sort
replaceAll(List, old, new)
列表 + 旧值 + 新值
替换所有匹配
返回是否发生替换
fill(List, obj)
列表 + 填充对象
全部填同一引用
——
copy(List dest, List src)
目标 + 源
复制(dest 需先有足够容量)
dest.size() < src.size() 抛异常
unmodifiableList/Map/Set(...)
原集合
只读视图 (改则抛 UnsupportedOperationException)
仅视图只读,原集合仍可改
synchronizedList/Map/Set(...)
原集合
线程安全包装
遍历仍需手动同步
singletonList(T) / singleton(T) / singletonMap(k,v)
单个元素
单元素不可变集合
节省内存
emptyList() / emptyMap() / emptySet()
无
共享的空不可变集合
返回同一常量实例
nCopies(int n, T o)
n:数量;o:元素
含 n 个相同引用的不可变列表
数组/元素相同引用
addAll(Collection, T...)
集合 + 变长元素
批量添加
——
disjoint(Collection1, Collection2)
两个集合
是否无交集
true=无交集
indexOfSubList(List, List) / lastIndexOfSubList
父列表 + 子列表
子列表首次/末次出现位置
——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import java.util.Collections;List<Integer> list = new ArrayList <>(Arrays.asList(3 , 1 , 4 , 1 , 5 )); Collections.addAll(list, xx,xxx,xxx,xxx); Collections.sort(list); Collections.sort(list, Comparator.reverseOrder()); Collections.reverse(list); Collections.shuffle(list); Collections.swap(list, 0 , 2 ); Collections.rotate(list, 2 ); int max = Collections.max(list); int min = Collections.min(list); int freq = Collections.frequency(list, 1 ); int pos = Collections.binarySearch(list, 4 ); List<String> immutable = Collections.unmodifiableList(list); Map<String, String> immutableMap = Collections.unmodifiableMap(map); List<String> syncList = Collections.synchronizedList(new ArrayList <>()); Map<K, V> syncMap = Collections.synchronizedMap(new HashMap <>()); List<String> single = Collections.singletonList("only" ); List<String> empty = Collections.emptyList(); Map<String, String> emptyMap = Collections.emptyMap(); List<String> copies = Collections.nCopies(5 , "hello" );
场景 :排序、安全包装、构造特殊集合、算法操作
Objects(对象工具类 JDK7+) java.util.Objects,null 安全的对象操作工具(避免 NullPointerException)。
方法
参数
返回值 / 行为
注意
equals(Object a, Object b)
两个对象
a==b 或 a.equals(b);两者任一为 null 也不 NPE
equals(null,"abc")→false
deepEquals(Object a, Object b)
两个对象
数组递归深度比较,多维数据时用
数组用更安全
isNull(Object o)
对象
o == null
用作 Predicate:list.removeIf(Objects::isNull)
nonNull(Object o)
对象
o != null
stream().filter(Objects::nonNull)
requireNonNull(T obj)
obj:待校验对象
非 null 返回 obj 本身,null 抛 NullPointerException
方法入参校验首选
requireNonNull(T obj, String msg)
obj + 异常消息
同上,null 时异常带 msg
提示更友好
requireNonNull(T obj, Supplier<String> msg)
obj + 延迟消息
同上,仅 null 时才构建消息(省性能)
JDK10+
requireNonNullElse(T obj, T defaultObj)
obj + 默认值
null 时返回 defaultObj
JDK9+
requireNonNullElseGet(T obj, Supplier)
obj + 默认值提供器
同上,延迟获取默认值
JDK9+
hashCode(Object o)
对象
o.hashCode(),null 返回 0
重写 hashCode 时常用
hash(Object... values)
变长字段
多字段组合 hash 值
hash(name, age) 一键组合
toString(Object o)
对象
o.toString(),null 返回 "null"
null 安全
toString(Object o, String nullDefault)
对象 + 默认值
null 时返回 nullDefault
——
compare(T a, T b, Comparator<? super T> c)
两对象 + 比较器
比较结果(null 安全,null 视为最小)
compare(a,b,naturalOrder())
checkIndex(int index, int length)
index:下标;length:长度
合法返回 index,否则抛 IndexOutOfBoundsException
JDK9+
checkFromToIndex(int from, int to, int len)
区间 + 长度
区间合法性校验
JDK9+
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.Objects;Objects.equals(null , "abc" ); Objects.equals("abc" , "abc" ); Objects.isNull(obj); Objects.nonNull(obj); Objects.requireNonNull(obj); Objects.requireNonNull(obj, "参数不能为空" ); Objects.requireNonNull(obj, () -> "延迟构建消息" ); int hash = Objects.hashCode(obj); int hash2 = Objects.hash(name, age, email); String s = Objects.toString(obj, "默认值" ); int cmp = Objects.compare(a, b, Comparator.naturalOrder());
场景 :重写 equals/hashCode、参数校验、null 安全操作
deepEquals使用场景
1 2 3 4 5 6 7 int [][] matrix1 = {{1 , 2 }, {3 , 4 }};int [][] matrix2 = {{1 , 2 }, {3 , 4 }};System.out.println(matrix1 == matrix2); System.out.println(matrix1.equals(matrix2)); System.out.println(Objects.equals(matrix1, matrix2)); System.out.println(Objects.deepEquals(matrix1, matrix2));
Math(数学工具类) java.lang.Math,静态工具类,方法均为 static,直接 Math.xxx() 调用。
方法
参数
返回值 / 行为
注意
abs(x)(int/long/float/double)
x:数值
|x| 绝对值
abs(Integer.MIN_VALUE) 仍返回负数(溢出)
absExact(x)(JDK15+)
x:数值
绝对值
溢出抛 ArithmeticException
max(a, b) / min(a, b)
两个数
较大 / 较小
重载支持 int/long/float/double
ceil(double a)
a:值
向上取整(返回 double,如 3.0)
ceil(3.2)→4.0
floor(double a)
a:值
向下取整
floor(3.8)→3.0
round(x)
float→int / double→long
四舍五入
round(3.5)→4,round(3.4)→3
pow(double a, double b)
a:底数;b:指数
a^b(double)
pow(2,10)→1024.0
sqrt(double a)
a:非负值
平方根
负数返回 NaN
cbrt(double a)
a:值
立方根
——
log(double a)
a:值
自然对数 ln(a)
log(Math.E)→1.0
log10(double a)
a:值
以 10 为底对数
——
exp(double a)
a:值
e^a
——
random()
无
[0.0, 1.0) 伪随机 double
线程安全但竞争时性能差,推荐 ThreadLocalRandom
sin/cos/tan(double)
弧度
三角函数
参数是弧度 非角度
toRadians(double ang) / toDegrees(double rad)
角度/弧度
互转
——
hypot(x, y)
两直角边
√(x²+y²)
避免中间溢出
signum(x)
x:值
-1 / 0 / 1
符号
copySign(m, s)
m:大小;s:符号来源
取 m 的绝对值 + s 的符号
——
nextUp/nextDown(x)
x:值
相邻浮点
——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Math.abs(-10 ); Math.max(3 , 7 ); Math.min(3 , 7 ); Math.ceil(3.2 ); Math.floor(3.8 ); Math.round(3.5f ); Math.pow(2 , 10 ); Math.sqrt(16 ); Math.log(Math.E); Math.random(); int random = (int )(Math.random() * (max - min + 1 )) + min;Math.absExact(Integer.MIN_VALUE);
场景 :数值计算、随机数生成、金融舍入、几何/三角函数
常见案例:保留 N 位小数(配合 BigDecimal 更安全)
1 2 double v = Math.round(3.14159 * 100 ) / 100.0 ;
Random 与 ThreadLocalRandom java.util.Random(伪随机数,线程共享时有竞争);ThreadLocalRandom(JDK7+,每个线程独立、无锁、高并发推荐);SecureRandom(密码学安全,用于 Token/盐值)。
方法
参数
返回值范围
注意
nextInt()
无
全 int 范围
含负数
nextInt(int bound)
bound:上界(必须 >0 )
[0, bound)
负数 bound 抛 IllegalArgumentException
nextInt(int origin, int bound)(JDK8+)
origin:下界(含);bound:上界(不含)
[origin, bound)
origin 可为负
nextLong() / nextLong(long bound)
同上(long 版)
全 long /[0, bound)
——
nextDouble()
无
[0.0, 1.0)
——
nextDouble(double bound)(JDK8+)
bound:上界
[0.0, bound)
——
nextDouble(origin, bound)(JDK8+)
区间
[origin, bound)
——
nextBoolean()
无
true/false 各 50%
——
nextFloat()
无
[0.0, 1.0)
——
nextBytes(byte[] bytes)
bytes:输出缓冲
填充随机字节
用于盐值
setSeed(long seed)
seed:随机种子
重置序列
同 seed 产生同序列(可复现)
new Random(long seed)
seed:种子
构造可复现随机源
测试常用
生成 [min, max] 闭区间整数(含两端) :int r = random.nextInt(max - min + 1) + min;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Random random = new Random ();int num = random.nextInt(100 ); double d = random.nextDouble(); boolean b = random.nextBoolean();int num2 = ThreadLocalRandom.current().nextInt(1 , 101 ); long l = ThreadLocalRandom.current().nextLong(1000 );import java.security.SecureRandom; import sun.security.provider.SecureRandom; SecureRandom secure = new SecureRandom ();byte [] bytes = new byte [16 ];secure.nextBytes(bytes);
场景 :生成验证码、抽奖、分布式ID辅助、密码盐值
常见案例:6 位短信验证码
1 int code = ThreadLocalRandom.current().nextInt(100000 , 1000000 );
Pattern 与 Matcher(正则表达式) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.regex.Pattern;import java.util.regex.Matcher; String str = "java返回给定的种子字节数量,JAva12该数量可使用此类用来将JAVA3088自身设置为种子的种子生成算法来计算。JaVa838此调用可用于设置其他随机数生成器的种子。" ; Pattern p = Pattern.compile("((?i)java)(\\d*)" ); Matcher mat = p.matcher(str); while (mat.find()){ System.out.println(mat.group() + "," + mat.group(1 ) + "," + mat.group(2 )); } String result = text.replaceAll("\\d+" , "*" ); String[] parts = "a,,b,c" .split("," , -1 ); public static boolean isPhone (String phone) { return phone != null && PHONE_PATTERN.matcher(phone).matches(); }
常见正则 :
用途
正则
手机号
^1[3-9]\\d{9}$
邮箱
^[\\w.-]+@[\\w.-]+\\.\\w{2,}$
身份证
^\\d{15}(\\d{2}[\\dXx])?$
IP地址
^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$
中文
^[\\u4e00-\\u9fa5]+$
URL
^https?://[\\w.-]+(:\\d+)?(/.*)?$
正则表达式语法详解 特殊的matches matches() = 自带 ^$
1. 基础元字符
元字符
含义
示例
.
匹配任意单个字符(除换行符)
a.c → “abc”、”a1c”
^
匹配字符串开头
^Hello → 以Hello开头
$
匹配字符串结尾
world$ → 以world结尾
\
转义特殊字符
\. → 匹配实际的点号
`
`
或(二选一)
2. 字符类
语法
含义
示例
[abc]
匹配a、b、c中任意一个
[aeiou] → 匹配元音
[^abc]
匹配除a、b、c外的任意字符
[^0-9] → 匹配非数字
[a-z]
匹配a到z的任意字符
[A-Za-z] → 匹配字母
[a-zA-Z0-9_]
等价于 \w
匹配单词字符
[a-b[d-g]]
匹配a-b或d-g
[aeiou] → 匹配元音
[a-b&&[d-g]]
匹配a-b且d-g
[aeiou] → 匹配元音
[a-z&&[^bc]]
匹配a-z且不是bc
[aeiou] → 匹配元音
[a-z&&[^m-p]]
匹配a-z且不是m-p
[aeiou] → 匹配元音
大小写忽略 1 2 3 4 5 6 String regex = "(?i)abc" ;System.out.println("abc" .matches(regex)); System.out.println("ABC" .matches(regex)); System.out.println("AbC" .matches(regex)); String regex2 = "a((?i)b)c" ;
分组 分组组号匹配
\\1 意思是(.)相同内容,如最终能匹配的是 a
Q: 匹配aaaxxxxxaaa怎么做
1 String str = ((.)\\2 *)).+\\1
替换$1
1 2 3 String a = "我要学学编编编和和和和和一" String b = a.replaceAll("(.)\\1+" , "$1" )
group(index)作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.regex.Pattern;import java.util.regex.Matcher;public class GroupDemo { public static void main (String[] args) { String text = "我的生日是 1990-05-20,今天是 2026-07-29" ; String regex = "(\\d{4})-(\\d{2})-(\\d{2})" ; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(text); while (matcher.find()) { System.out.println("完整日期: " + matcher.group(0 )); System.out.println(" 年: " + matcher.group(1 )); System.out.println(" 月: " + matcher.group(2 )); System.out.println(" 日: " + matcher.group(3 )); System.out.println("---" ); } } } 完整日期: 1990 -05 -20 年: 1990 月: 05 日: 20 ---
Split分割参数第2个细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String str = "a,,b,c," ; String[] parts1 = str.split("," , -1 ); System.out.println(Arrays.toString(parts1)); String[] parts2 = str.split("," , 0 ); System.out.println(Arrays.toString(parts2)); String[] parts3 = str.split("," ); System.out.println(Arrays.toString(parts3));
3. 预定义字符类
语法
含义
等价写法
\d
数字
[0-9]
\D
非数字
[^0-9]
\w
单词字符(字母、数字、下划线)
[a-zA-Z0-9_]
\W
非单词字符
[^a-zA-Z0-9_]
\s
空白字符(空格、制表符、换行等)
[ \t\n\r\f]
\S
非空白字符
[^ \t\n\r\f]
\b
单词边界
\bword\b → 精确匹配单词
\B
非单词边界
—
Java 注意 :Java 字符串中 \ 本身需要转义,所以正则中的 \d 在代码中要写成 "\\d"。
4. 量词(Quantifiers)
量词
含义
示例
*
0次或多次
\d* → 匹配0个或多个数字
+
1次或多次
\d+ → 至少1个数字
?
0次或1次
colou?r → “color”或”colour”
{n}
恰好n次
\d{4} → 4位数字
{n,}
至少n次
\d{2,} → 至少2位数字
{n,m}
n到m次
\d{6,18} → 6到18位数字
贪婪 vs 懒惰 :
模式
含义
示例(对 “aabab”)
a.*b(贪婪,默认)
尽可能多匹配
匹配 “aabab”
a.*?b(懒惰)
尽可能少匹配
匹配 “aab”
a.*+b(占有型)
贪婪且不回溯
不匹配(不回溯)
5. 分组与捕获 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})" );Matcher m = p.matcher("2026-07-21" );if (m.matches()) { m.group(0 ); m.group(1 ); m.group(2 ); m.group(3 ); } Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})" );Matcher m = p.matcher("2026-07-21" );if (m.matches()) { m.group("year" ); m.group("month" ); m.group("day" ); } Pattern p = Pattern.compile("(?:https?://)([\\w.-]+)" );Matcher m = p.matcher("https://www.example.com" );if (m.find()) { m.group(1 ); }
6. 零宽断言(Lookaround)
语法
名称
含义
示例
(?=X)
正向前瞻
后面紧跟X
\d+(?=元) → “100元”中的”100”
(?:X)
正向前瞻
后面紧跟X
\d+(?:元) → “100元”中的”100元”
(?!X)
负向前瞻
后面不是X
\d+(?!元) → “100个”中的”100”
(?<=X)
正向后顾
前面紧跟X
(?<=\$)\d+ → “$50”中的”50”
(?<!X)
负向后顾
前面不是X
(?<!\$)\d+ → “¥50”中的”50”
1 2 3 4 5 6 7 8 9 10 11 Pattern p = Pattern.compile("(?<=[¥$])\\d+(\\.\\d+)?" );Matcher m = p.matcher("商品价格:¥199.50,折扣价$15.99" );while (m.find()) { System.out.println(m.group()); } String regex = "^(?=.*[a-zA-Z])(?=.*\\d).{8,}$" ;"abc12345" .matches(regex); "abcdefgh" .matches(regex);
7. 反向引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Pattern p = Pattern.compile("\\b(\\w+)\\s+\\1\\b" );Matcher m = p.matcher("the the quick fox fox" );while (m.find()) { System.out.println(m.group()); } Pattern p = Pattern.compile("<(\\w+)>.*?</\\1>" );Matcher m = p.matcher("<b>bold</b> and <i>italic</i>" );while (m.find()) { System.out.println(m.group()); }
8. Pattern 标志(Flags)
标志
内联写法
含义
Pattern.CASE_INSENSITIVE
(?i)
忽略大小写
Pattern.MULTILINE
(?m)
^ $ 匹配每行的开头/结尾
Pattern.DOTALL
(?s)
. 也匹配换行符
Pattern.COMMENTS
(?x)
允许空白和注释,增强可读性
Pattern.UNICODE_CHARACTER_CLASS
(?U)
\w \d 等适用 Unicode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Pattern p = Pattern.compile("hello world" , Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); Pattern p = Pattern.compile("(?im)hello world" );Pattern p = Pattern.compile(""" ^ # 行首 (?=.*[A-Z]) # 至少一个大写字母 (?=.*[a-z]) # 至少一个小写字母 (?=.*\\d) # 至少一个数字 (?=.*[@#$%^&+=!]) # 至少一个特殊字符 .{8,20} # 总长度 8-20 $ # 行尾 """ , Pattern.COMMENTS);
9. Java 常用操作汇总 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 boolean isValid = Pattern.matches("\\d+" , "12345" ); boolean isValid = "12345" .matches("\\d+" );Pattern p = Pattern.compile("\\d+" );Matcher m = p.matcher("价格100元,数量5个,总计500元" );List<String> results = new ArrayList <>(); while (m.find()) { results.add(m.group()); } String result = "hello 123 world 456" .replaceAll("\\d+" , "*" );String result = "2026-07-21" .replaceAll("(\\d{4})-(\\d{2})-(\\d{2})" , "$1年$2月$3日" );Pattern p = Pattern.compile("\\d+" );Matcher m = p.matcher("a1b22c333" );StringBuilder sb = new StringBuilder ();while (m.find()) { m.appendReplacement(sb, "[" + m.group().length() + "位数]" ); } m.appendTail(sb); String[] parts = "one,two,,four" .split("," ); String[] parts = "one,two,,four" .split("," , -1 ); String userInput = "price is $100 (USD)" ;String escaped = Pattern.quote(userInput); Pattern p = Pattern.compile(escaped);
10. 性能与最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 private static final Pattern PATTERN = Pattern.compile("\\d+" );for (String s : list) { s.matches("\\d+" ); } private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d+" );for (String s : list) { DIGIT_PATTERN.matcher(s).matches(); }
避免灾难性回溯 :
1 2 3 4 5 6 7 Pattern.compile("(a+)+b" ); Pattern.compile("(\\w+\\s*)+$" ); Pattern.compile("a+b" ); Pattern.compile("[\\w\\s]+$" );
建议 :
复杂正则加注释(使用 Pattern.COMMENTS)
对用户输入做 Pattern.quote() 转义后再拼接正则
设置超时(Java 9+ 可用 Matcher 的中断机制;或在单独线程中执行并设超时)
优先使用 String.contains()/startsWith()/endsWith() 等简单方法,正则只在确实需要模式匹配时使用
Date 获取日期
1 2 3 public Date(int year, int month, int day)
已过时。 使用构造方法 Date(long date) 替代
构造一个使用给定 year、month 和 day 初始化的 Date 对象。如果给定参数超出范围,则结果是不确定的。
参数: year - year 减去 1900,它必须是 0 到 8099 之间的数。(注意,8099 是由 9999 减去 1900 得到的。)month - 0 到 11 之间的数 day - 1 到 31 之间的数
常见: yyyy-MM-dd HH:mm:ss
格式化和解析日期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.text.SimpleDateFormat;import java.text.ParseException;import java.util.Date;Date now = new Date ();SimpleDateFormat sdf1 = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );String s1 = sdf1.format(now); SimpleDateFormat sdf2 = new SimpleDateFormat ("yyyy年MM月dd日 E a hh:mm" );String s2 = sdf2.format(now); SimpleDateFormat sdf3 = new SimpleDateFormat ("yyyyMMddHHmmssSSS" );String s3 = sdf3.format(now); SimpleDateFormat parser = new SimpleDateFormat ("yyyy-MM-dd" );try { Date date = parser.parse("2026-12-25" ); System.out.println(date); } catch (ParseException e) { e.printStackTrace(); } long timestamp = System.currentTimeMillis();String fromTs = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ).format(new Date (timestamp));long toTs = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ) .parse("2026-07-30 14:30:00" ).getTime(); String result = new SimpleDateFormat ("yyyy-MM-dd" ).format(new Date ());private static final ThreadLocal<SimpleDateFormat> TL_SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" )); String safe = TL_SDF.get().format(new Date ());
常见格式模板速查 :
模板
输出示例
场景
yyyy-MM-dd
2026-07-30
日期存储、接口传参
yyyy-MM-dd HH:mm:ss
2026-07-30 14:30:00
日志、数据库
yyyy/MM/dd
2026/07/30
日式/财务格式
yyyyMMdd
20260730
文件名、批次号
yyyyMMddHHmmssSSS
20260730143000123
流水号、唯一标识
yyyy年M月d日
2026年7月30日
中文展示(M/d 不补零)
EEE, dd MMM yyyy
Wed, 30 Jul 2026
HTTP 日期头(需设 Locale.ENGLISH)
Calendar 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 Calendar cal = Calendar.getInstance(); Calendar cal2 = Calendar.getInstance();cal2.setTime(new Date ()); cal2.setTimeInMillis(System.currentTimeMillis()); int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH); int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int hour12 = cal.get(Calendar.HOUR); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); int dayOfYear = cal.get(Calendar.DAY_OF_YEAR); int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR); int realMonth = cal.get(Calendar.MONTH) + 1 ; cal.set(Calendar.YEAR, 2025 ); cal.set(Calendar.MONTH, Calendar.DECEMBER); cal.set(Calendar.DAY_OF_MONTH, 25 ); cal.set(Calendar.HOUR_OF_DAY, 14 ); cal.set(Calendar.MINUTE, 30 ); cal.set(Calendar.SECOND, 0 ); cal.set(2025 , Calendar.DECEMBER, 25 ); cal.set(2025 , Calendar.DECEMBER, 25 , 14 , 30 , 0 ); cal.add(Calendar.DAY_OF_MONTH, 7 ); cal.add(Calendar.MONTH, -3 ); cal.add(Calendar.YEAR, 1 ); cal.add(Calendar.HOUR_OF_DAY, -2 ); cal.roll(Calendar.DAY_OF_MONTH, 40 ); Date date = cal.getTime();Calendar cal3 = Calendar.getInstance();cal3.setTime(date); long ts = cal.getTimeInMillis();Calendar calA = Calendar.getInstance();Calendar calB = Calendar.getInstance();calB.add(Calendar.DAY_OF_MONTH, 1 ); calA.before(calB); calA.after(calB); calA.compareTo(calB); Calendar firstDay = Calendar.getInstance();firstDay.set(Calendar.DAY_OF_MONTH, 1 ); firstDay.set(Calendar.HOUR_OF_DAY, 0 ); firstDay.set(Calendar.MINUTE, 0 ); firstDay.set(Calendar.SECOND, 0 ); firstDay.set(Calendar.MILLISECOND, 0 ); Calendar lastDay = Calendar.getInstance();lastDay.set(Calendar.DAY_OF_MONTH, lastDay.getActualMaximum(Calendar.DAY_OF_MONTH)); lastDay.set(Calendar.HOUR_OF_DAY, 23 ); lastDay.set(Calendar.MINUTE, 59 ); lastDay.set(Calendar.SECOND, 59 ); lastDay.set(Calendar.MILLISECOND, 999 ); int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH); String[] weekDays = {"周日" ,"周一" ,"周二" ,"周三" ,"周四" ,"周五" ,"周六" }; int weekIndex = cal.get(Calendar.DAY_OF_WEEK) - 1 ; String weekName = weekDays[weekIndex];Calendar c1 = Calendar.getInstance();Calendar c2 = Calendar.getInstance();c2.set(2026 , Calendar.DECEMBER, 31 ); long diffMs = c2.getTimeInMillis() - c1.getTimeInMillis();long diffDays = diffMs / (1000 * 60 * 60 * 24 );
注意事项 :
要点
说明
MONTH 从 0 开始
0=一月,11=十二月,取值后需 +1
DAY_OF_WEEK 从 1 开始
1=周日,2=周一,…,7=周六
add vs roll
add 会进位影响更大字段;roll 只改当前字段不进位
线程安全
Calendar 非线程安全,不要做 static 共享
推荐
JDK8+ 优先使用 LocalDate/LocalDateTime,更简洁且线程安全
JDK8 & JDK<8对比 Java 中月份取值不统一,根源在于新旧两套日期 API 的设计差异。简单来说:
旧版 (java.util.Date, Calendar) :月份从 0 开始,即 0 代表一月,11 代表十二月。
新版 (java.time,Java 8+) :月份从 1 开始,与我们日常认知一致。
旧版 (Calendar) : 1 = 星期日 ,2 = 星期一 …… 7 = 星期六(美国标准)。
新版 (java.time) : 1 = 星期一 ,2 = 星期二 …… 7 = 星期日(ISO国际标准)
ZoneId 时区 Instant 时间戳 DateTimeFormatter 是 JDK8 java.time 包提供的日期时间格式化器,不可变且线程安全 ,可在多线程环境下安全地定义为 static final 常量复用。相比旧版 SimpleDateFormat(非线程安全,多线程需 ThreadLocal 包裹),DateTimeFormatter 是推荐方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" );DateTimeFormatter fmt2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒" );LocalDate inputDate = LocalDate.parse(str, DateTimeFormatter.ofPattern("[yyyy/MM/dd][yyyy-MM-dd][yyyyMMdd]" ));DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME; DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE; DateTimeFormatter basicIso = DateTimeFormatter.BASIC_ISO_DATE; DateTimeFormatter isoOffset = DateTimeFormatter.ISO_OFFSET_DATE_TIME; LocalDateTime now = LocalDateTime.now();String s1 = now.format(fmt); String s2 = fmt.format(now); LocalDateTime t1 = LocalDateTime.parse("2026-08-05 14:30:00" , fmt);LocalDate d1 = LocalDate.parse("2026-08-05" , DateTimeFormatter.ISO_LOCAL_DATE);LocalDateTime t2 = LocalDateTime.parse("2026-08-05T14:30:00" );DateTimeFormatter cnFmt = DateTimeFormatter .ofPattern("yyyy MMMM dd EEEE" , Locale.CHINA); String cn = now.format(cnFmt); ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai" ));DateTimeFormatter zonedFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz" );String zStr = zdt.format(zonedFmt); private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static String format (LocalDateTime time) { return time.format(DATE_TIME_FMT); }
LocalDate 年月日(无时分秒) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 LocalDate today = LocalDate.now(); LocalDate date = LocalDate.of(2026 , 8 , 6 ); LocalDate parsed = LocalDate.parse("2026-08-06" ); int year = today.getYear();int month = today.getMonthValue(); int day = today.getDayOfMonth(); DayOfWeek dow = today.getDayOfWeek();LocalDate tomorrow = today.plusDays(1 ); LocalDate lastMonth = today.minusMonths(1 ); LocalDate firstDay = today.withDayOfMonth(1 ); boolean isLeap = today.isLeapYear(); boolean before = today.isBefore(date);
LocalTime 时分秒 1 2 3 4 5 6 7 LocalTime now = LocalTime.now(); LocalTime t = LocalTime.of(14 , 30 , 0 ); int hour = now.getHour(); int min = now.getMinute(); LocalTime later = now.plusHours(2 ).plusMinutes(30 );
LocalDateTime年月日 时分秒 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 LocalDateTime now = LocalDateTime.now(); LocalDateTime specific = LocalDateTime.of(2026 , 8 , 5 , 14 , 30 , 0 ); LocalDate today = LocalDate.now(); LocalTime time = LocalTime.now(); LocalDateTime combined = LocalDateTime.of(today, time); LocalDateTime parsed = LocalDateTime.parse("2026-08-05T14:30:00" );LocalDateTime parsed2 = LocalDateTime.parse("2026-08-05 14:30:00" , DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); int year = now.getYear(); int month = now.getMonthValue(); int day = now.getDayOfMonth(); int hour = now.getHour(); int minute = now.getMinute(); int second = now.getSecond(); DayOfWeek dayOfWeek = now.getDayOfWeek(); LocalDateTime plusDays = now.plusDays(7 ); LocalDateTime minusMonths = now.minusMonths(3 ); LocalDateTime plusHours = now.plusHours(2 ); LocalDateTime plusYears = now.plusYears(1 ); LocalDateTime modified = now.withYear(2025 ) .withMonth(12 ) .withDayOfMonth(25 ) .withHour(14 ) .withMinute(30 ) .withSecond(0 ); DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" );String formatted = now.format(fmt); LocalDateTime fromStr = LocalDateTime.parse("2026-08-05 14:30:00" , fmt);boolean isBefore = now.isBefore(specific); boolean isAfter = now.isAfter(specific);boolean isEqual = now.isEqual(specific);Duration duration = Duration.between(now, specific);long hours = duration.toHours();long minutes = duration.toMinutes();Period period = Period.between(today, LocalDate.of(2026 , 12 , 31 ));int diffYears = period.getYears();int diffMonths = period.getMonths();int diffDays = period.getDays();long daysBetween = ChronoUnit.DAYS.between(today, LocalDate.of(2026 , 12 , 31 ));Date date = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());LocalDateTime fromDate = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());long timestamp = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();LocalDateTime fromTs = LocalDateTime.ofInstant( Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); Calendar cal = Calendar.getInstance();LocalDateTime fromCal = LocalDateTime.ofInstant(cal.toInstant(), ZoneId.systemDefault());LocalDateTime firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth()) .withHour(0 ).withMinute(0 ).withSecond(0 ).withNano(0 ); LocalDateTime lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth()) .withHour(23 ).withMinute(59 ).withSecond(59 ); int daysInMonth = now.toLocalDate().lengthOfMonth();boolean isLeap = now.toLocalDate().isLeapYear();LocalDateTime startOfDay = today.atStartOfDay(); LocalDateTime endOfDay = today.atTime(LocalTime.MAX);
Period 年月日间隔 1 2 3 4 5 6 7 Period p = Period.between(LocalDate.of(2020 , 1 , 1 ), LocalDate.of(2026 , 8 , 6 ));int years = p.getYears(); int months = p.getMonths(); int days = p.getDays();
Duration 时间间隔 1 2 3 4 5 6 7 8 9 10 LocalTime start = LocalTime.of(9 , 0 , 0 );LocalTime end = LocalTime.of(17 , 30 , 0 );Duration d = Duration.between(start, end); long hours = d.toHours();long minutes = d.toMinutes(); long seconds = d.getSeconds();long millis = d.toMillis(); Duration twoHours = Duration.ofHours(2 );
ChronoUnit 最常用 计算时间间隔 1 2 3 4 5 6 7 8 9 10 11 12 LocalDate start = LocalDate.of(2020 , 1 , 1 );LocalDate end = LocalDate.of(2026 , 8 , 6 );long days = ChronoUnit.DAYS.between(start, end); long months = ChronoUnit.MONTHS.between(start, end); long years = ChronoUnit.YEARS.between(start, end); LocalDateTime t1 = LocalDateTime.of(2026 , 8 , 6 , 9 , 0 , 0 );LocalDateTime t2 = LocalDateTime.of(2026 , 8 , 6 , 17 , 30 , 0 );long hours = ChronoUnit.HOURS.between(t1, t2); long minutes = ChronoUnit.MINUTES.between(t1, t2);
Period vs ChronoUnit :Period.between() 返回”几年几月几天”拆开的余数;ChronoUnit.DAYS.between() 返回总天数 。大部分场景用 ChronoUnit 更方便。
YearMonth 1 2 3 4 5 6 7 8 9 10 11 import java.time.LocalDate;import java.time.YearMonth;public class Main { public static void main (String[] args) { LocalDate lastDay = YearMonth.now().atEndOfMonth(); System.out.println("本月最后一天:" + lastDay); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" );String str = LocalDateTime.now().format(fmt);LocalDateTime dt = LocalDateTime.parse("2026-08-06 14:30:00" , fmt);DateTimeFormatter.ISO_LOCAL_DATE; DateTimeFormatter.ISO_LOCAL_DATE_TIME;
Instant 时间戳(机器时间,UTC) 1 2 3 4 5 6 7 8 9 10 11 Instant now = Instant.now(); long epochMilli = now.toEpochMilli(); Instant ins = Instant.ofEpochMilli(1786168225123L ); LocalDateTime ldt = LocalDateTime.ofInstant(ins, ZoneId.systemDefault());Instant back = ldt.atZone(ZoneId.systemDefault()).toInstant();
ZonedDateTime 带时区 1 2 3 4 5 6 ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai" ));ZonedDateTime tokyo = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo" ));
TemporalAdjusters 日期调节器 1 2 3 4 5 6 7 8 9 10 LocalDate today = LocalDate.of(2026 , 8 , 6 ); LocalDate firstDay = today.with(TemporalAdjusters.firstDayOfMonth());LocalDate lastDay = today.with(TemporalAdjusters.lastDayOfMonth());LocalDate nextMon = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
新旧 API 互转 1 2 3 4 5 6 7 8 9 10 11 12 13 Date old = new Date (); LocalDateTime ldt = LocalDateTime.ofInstant(old.toInstant(), ZoneId.systemDefault());Date newDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());long ts = System.currentTimeMillis(); LocalDateTime fromTs = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault());
注意事项 :
要点
说明
线程安全
不可变对象,可放心定义为 static final 全局复用
大小写敏感
MM(月)与 mm(分)不同;HH(24h)与 hh(12h)不同
月份从 1 开始
不同于 SimpleDateFormat 历史陷阱,java.time 一贯 1~12
解析严格
模式与字符串不匹配会抛 DateTimeParseException
本地化
需要中文月份/星期时务必传入 Locale.CHINA,否则按默认 Locale
Arrays 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int [] arr = {3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 };String s = Arrays.toString(arr); Arrays.sort(arr); Integer[] arrBox = {3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 }; Arrays.sort(arrBox, Collections.reverseOrder()); Arrays.sort(arrBox, new Comparator <Integer>() { @Override public int compare (Integer o1, Integer o2) { return o2 - o1; } }); int [] part = {5 , 3 , 1 , 4 , 2 };Arrays.sort(part, 0 , 3 ); int idx = Arrays.binarySearch(arr, 5 ); int [] copy = Arrays.copyOf(arr, 3 ); int [] padded = Arrays.copyOf(arr, 10 ); int [] range = Arrays.copyOfRange(arr, 2 , 5 ); int [] empty = new int [5 ];Arrays.fill(empty, 7 );
注意点 :
binarySearch 前必须先 sort,否则结果不确定
sort/fill 是原地操作 ,改变原数组;copyOf/copyOfRange 返回新数组
copyOf 长度超过原数组时,不足部分补默认值(int 补 0,对象补 null)
关于sort 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main (String[] args) { List<Stu> list = Arrays.asList( new Stu ("xiao hong" , 22 ), new Stu ("xiao jiao" , 18 ), new Stu ("xiao qin" , 30 ), new Stu ("xiao xu" , 18 ), new Stu ("xiao min" , 22 ), new Stu ("xiao hong" , 22 ) ); System.out.println(list); System.out.println( new Stu ("xiao hong" , 22 ).equals( new Stu ("xiao hong" , 22 ))); list.sort((a,b)->{ if (a.getAge() != b.getAge()){ return a.getAge() - b.getAge(); } return a.getName().compareTo(b.getName()); }); System.out.println(list); }
Collection(集合根接口) java.util.Collection 是 List、Set、Queue 的父接口,定义了对集合的通用操作。
基本使用
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ArrayList;import java.util.Collection;Collection<String> list = new ArrayList <>(); list.add("a" ); list.add("b" ); list.addAll(list2); list.remove("a" ); list.contains("b" ); list.size(); list.isEmpty(); list.clear();
各种遍历方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Collection<String> c = new ArrayList <>(Arrays.asList("a" , "b" , "c" )); for (String s : c) { System.out.println(s); } Iterator<String> it = c.iterator(); while (it.hasNext()) { String s = it.next(); if ("b" .equals(s)) it.remove(); } c.forEach(s -> System.out.println(s)); c.forEach(System.out::println); String[] arr = c.toArray(new String [0 ]); for (String s : arr) { System.out.println(s); }c.forEach(new Consumer <String>() { @Override public void accept (String s) { System.out.println(s); } });
遍历方式选择建议
方式
适用场景
for-each
普通读取,不需要删除
Iterator
需要在遍历中删除元素
forEach + lambda
代码简洁、一次性消费
Java 集合框架关系图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ┌──────────────────────────────────────────────┐ │ Iterable<E> │ └─────────────────────┬────────────────────────┘ │ ┌─────────────────────▼────────────────────────┐ │ Collection<E> │ └───────┬─────────────┬───────────────┬────────┘ │ │ │ ┌─────────────────▼──┐ ┌──────▼──────┐ ┌────▼─────────┐ │ List<E> │ │ Set<E> │ │ Queue<E> │ │ (有序、可重复) │ │(无序、不重复)│ │ (先进先出) │ └──┬──────────┬──────┘ └──┬──────┬───┘ └──┬───────────┘ │ │ │ │ │ ┌────────────▼───┐ ┌───▼──────────┐ │ │ ┌────▼──────────┐ │ ArrayList<E> │ │ LinkedList<E> │ │ │ │ Deque<E> │ │ (数组实现,查快) │ │(链表查询慢增删快) │ │ │ │ (双端队列) │ └────────────────┘ └──────────────┘ │ │ └──┬────────────┘ │ │ │ ┌────────────▼──┐ ┌─▼───────────┐ ┌─────────────┐ │ HashSet<E> │ │ TreeSet<E> │ │ArrayDeque<E>│ │(哈希,最快O(1))│ │(红黑树,有序) │ │ (栈/队列) │ └───────┬───────┘ └─────────────┘ └─────────────┘ │ ┌────────▼────────┐ │LinkedHashSet<E> │ │(保持插入顺序) │ └─────────────────┘ ┌──────────────────────────────────────────┐ │ Map<K,V> │ │ (键值对,key不重复) │ └───────┬──────────┬───────────┬───────────┘ │ │ │ ┌──────────▼───┐ ┌───▼────────┐ ┌▼────────────┐ │ HashMap<K,V> │ │TreeMap<K,V> │ │Hashtable<K,V│ │(哈希,最常用) │ │(红黑树,有序)│ │(线程安全,旧) │ └──────┬───────┘ └────────────┘ └─────────────┘ │ ┌────────▼─────────┐ │LinkedHashMap<K,V> │ │(保持插入/访问顺序) │ └──────────────────┘
💡 选择建议 :
随机访问多 → ArrayList;频繁增删 → LinkedList
去重不关心顺序 → HashSet;去重且排序 → TreeSet;去重且保持插入顺序 → LinkedHashSet
键值映射 → HashMap;需按 key 排序 → TreeMap;需保持插入顺序 → LinkedHashMap
栈/队列 → ArrayDeque(不要用 Stack,已过时)
List(有序集合接口) List 继承 Collection,有序 、允许重复 、支持索引访问 。
遍历: for( x: list) for itrator lambda
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import java.util.*;List<String> arrayList = new ArrayList <>(); arrayList.add("A" ); arrayList.add(0 , "B" ); arrayList.get(0 ); arrayList.set(0 , "C" ); arrayList.remove(0 ); arrayList.remove("A" ); arrayList.contains("C" ); arrayList.indexOf("C" ); arrayList.lastIndexOf("C" ); arrayList.subList(0 , 2 ); arrayList.size(); arrayList.isEmpty(); arrayList.clear(); List<Integer> list = new ArrayList <>(); list.addAll(Collections.nCopies(70 , 1 )); list.addAll(Collections.nCopies(30 , 2 )); List<String> list = new ArrayList <>(Arrays.asList("aaa" , "bbb" , "aaa" )); list.remove("aaa" ); System.out.println(list); list.removeAll(Collections.singleton("aaa" )); list.removeIf(s -> s.equals("aaa" )); (Java 8 +,最灵活) List<Integer> numbers = new ArrayList <>(Arrays.asList(10 , 20 , 30 )); numbers.remove(1 ); numbers.remove(Integer.valueOf(1 )); LinkedList<String> linkedList = new LinkedList <>(); linkedList.add("A" ); linkedList.addFirst("B" ); linkedList.addLast("C" ); linkedList.getFirst(); linkedList.getLast(); linkedList.removeFirst(); linkedList.removeLast(); linkedList.peek(); linkedList.poll(); linkedList.push("D" ); linkedList.pop();
列表迭代器 listItrator,列表需要遍历中增加元素时用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { List<Integer> arr = new ArrayList <>(); arr.add(1 ); arr.add(2 ); arr.add(3 ); arr.add(4 ); ListIterator it = arr.listIterator(); while (it.hasNext()){ Object curData = it.next(); if (curData.equals(3 )){ it.add(100 ); } } System.out.println(arr); }
ArrayList vs LinkedList 对比
特性
ArrayList
LinkedList
底层结构
动态数组
双向链表
随机访问
O(1) 快
O(n) 慢
头部增删
O(n) 慢(要移位)
O(1) 快
尾部增删
均摊 O(1)
O(1)
内存占用
紧凑
每个节点多两个指针
适用场景
读多写少、按索引访问
频繁头部增删、队列/栈
Set(不重复集合接口) Set 继承 Collection,不允许重复元素 (通过 equals+hashCode 判断)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import java.util.*;Set<String> hashSet = new HashSet <>(); hashSet.add("A" ); hashSet.add("B" ); hashSet.add("A" ); hashSet.contains("A" ); hashSet.remove("A" ); hashSet.size(); Set<String> s1 = new HashSet <>(Arrays.asList("A" , "B" , "C" )); Set<String> s2 = new HashSet <>(Arrays.asList("B" , "C" , "D" )); Set<String> union = new HashSet <>(s1); union.addAll(s2); Set<String> inter = new HashSet <>(s1); inter.retainAll(s2); Set<String> diff = new HashSet <>(s1); diff.removeAll(s2); TreeSet<Integer> treeSet = new TreeSet <>(); treeSet.add(5 ); treeSet.add(1 ); treeSet.add(3 ); treeSet.first(); treeSet.last(); treeSet.lower(3 ); treeSet.higher(3 ); treeSet.floor(3 ); treeSet.ceiling(3 ); treeSet.headSet(3 ); treeSet.tailSet(3 ); treeSet.subSet(1 , 5 ); treeSet.pollFirst(); treeSet.pollLast(); TreeSet<String> customSet = new TreeSet <>(Comparator.comparing(String::length)); customSet.addAll(Arrays.asList("apple" , "hi" , "cat" )); Set<String> linkedSet = new LinkedHashSet <>(); linkedSet.add("C" ); linkedSet.add("A" ); linkedSet.add("B" );
HashSet vs TreeSet vs LinkedHashSet 对比
特性
HashSet
TreeSet
LinkedHashSet
底层结构
HashMap
红黑树
HashMap+链表
是否有序
无序
自然/比较器排序
插入顺序
性能
O(1)
O(log n)
O(1)
null元素
允许1个
不允许
允许1个
适用场景
通用去重
需要排序去重
去重且保序
LinkedList 常用讲解 LinkedList 底层是双向链表 ,每个节点既存数据、又存前后指针,因此既能当 List 用,又能当 Queue / Deque(栈、队列)用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.LinkedList;LinkedList<String> list = new LinkedList <>(); list.add("A" ); list.add(0 , "B" ); list.get(0 ); list.set(0 , "C" ); list.remove(0 ); list.remove("A" ); list.contains("C" ); list.size(); list.offer("X" ); list.poll(); list.peek(); list.addFirst("Y" ); list.addLast("Z" ); list.getFirst(); list.getLast(); list.removeFirst(); list.removeLast(); list.push("P" ); list.pop();
常用场景
场景
推荐方法
说明
频繁在头部/中间 增删
LinkedList
链表改动指针即可,O(1)
当队列 用
offer / poll / peek
FIFO
当栈 用
push / pop
LIFO
频繁随机按索引访问
ArrayList
LinkedList.get(i) 要遍历,慢
💡 口诀 :增删多用 LinkedList,查询多用 ArrayList。
HashSet / LinkedHashSet 常见使用与适用需求 两者都不允许重复元素 (靠 equals + hashCode 去重),区别只在遍历顺序:
HashSet:无序 (增删查 O(1),性能最好,日常默认选它)
LinkedHashSet:在 HashSet 基础上用双向链表记录插入顺序 ,遍历时按添加顺序输出(增删查仍是 O(1),内存略多)
1 2 Set<String> hashSet = new HashSet <>(); Set<String> linkedSet = new LinkedHashSet <>();
1. 数组 / List 去重(最常用场景) 1 2 3 4 5 6 7 8 9 List<String> list = Arrays.asList("a" , "b" , "a" , "c" , "b" ); List<String> distinct = new ArrayList <>(new HashSet <>(list)); List<String> ordered = new ArrayList <>(new LinkedHashSet <>(list));
适用需求 :接口返回列表去重、Excel 批量导入剔除重复行、日志关键字去重展示。
2. 快速判断「是否已存在 / 是否重复」(O(1)) 1 2 3 4 5 6 Set<String> seen = new HashSet <>(); for (String item : items) { if (!seen.add(item)) { System.out.println("重复元素:" + item); } }
⚠️ 比 List.contains(item) 的 O(n) 快得多,数据量大时务必用 Set 。
3. 去重 + 计数(统计出现次数) 1 2 3 4 5 6 Map<String, Integer> count = new HashMap <>(); for (String s : list) { count.put(s, count.getOrDefault(s, 0 ) + 1 ); } Set<String> keys = count.keySet();
4. 幂等 / 「已处理」标记(防重复提交、防重复消费) 1 2 3 4 5 6 7 Set<Long> processed = new HashSet <>(); if (processed.contains(msgId)) { return ; } doBusiness(msgId); processed.add(msgId);
适用需求 :MQ 消息幂等、定时任务防重跑、表单重复提交拦截。
5. 集合交 / 并 / 差运算 1 2 3 4 5 6 Set<String> s1 = new HashSet <>(Arrays.asList("A" , "B" , "C" )); Set<String> s2 = new HashSet <>(Arrays.asList("B" , "C" , "D" )); Set<String> union = new HashSet <>(s1); union.addAll(s2); Set<String> inter = new HashSet <>(s1); inter.retainAll(s2); Set<String> diff = new HashSet <>(s1); diff.removeAll(s2);
6. 保序去重 + 顺序敏感的场景(用 LinkedHashSet) 1 2 3 4 LinkedHashSet<String> tags = new LinkedHashSet <>(); tags.add("价格" ); tags.add("销量" ); tags.add("价格" );
适用需求 :筛选条件去重保序、最近浏览记录(去重且按访问顺序)、步骤/路径去重。
HashSet vs LinkedHashSet 选型
需求
选哪个
只要去重,不关心顺序
HashSet
去重 + 保持添加顺序
LinkedHashSet
海量数据判重 / 幂等
HashSet
去重 + 需要排序
TreeSet
去重 + 需要顺序输出到前端
LinkedHashSet
💡 记忆法 :要「快」用 HashSet;要「快且按原顺序」用 LinkedHashSet;要「排序」才上 TreeSet。
TreeSet 常见使用与适用需求 TreeSet 基于红黑树 实现,特点是元素自动排序 (自然顺序或可传入 Comparator),增删查为 O(log n)。**不允许 null**(排序时无比较基准)。
1 2 3 4 5 6 7 8 9 TreeSet<Integer> treeSet = new TreeSet <>(); treeSet.add(5 ); treeSet.add(1 ); treeSet.add(3 ); TreeSet<String> byLen = new TreeSet <>(Comparator.comparing(String::length)); byLen.addAll(Arrays.asList("apple" , "hi" , "cat" ));
0. 两种排序方式(核心) TreeSet 的排序只来自两个来源:元素自身实现 Comparable 或 **构造时传入 Comparator**。两者二选一,同时传 Comparator 时以它为准。
① 自然排序(元素实现 Comparable)
1 2 3 4 5 6 7 8 class Student implements Comparable <Student> { int score; public int compareTo (Student o) { return Integer.compare(this .score, o.score); } } TreeSet<Student> set = new TreeSet <>();
② 定制排序(构造时传 Comparator,不用改元素类)
1 2 3 4 5 6 7 8 9 TreeSet<Integer> desc = new TreeSet <>(Comparator.reverseOrder()); desc.addAll(Arrays.asList(3 , 1 , 2 )); TreeSet<User> users = new TreeSet <>( Comparator.comparingInt((User u) -> u.getScore()).reversed() .thenComparingInt(User::getAge) );
💡 优先用 ② 定制排序:Comparator 写法灵活、不改实体类、可随时换规则;只有排序规则固定且实体类可自由修改时才用 ①。
1. 去重 + 自动排序(最常用) 1 2 3 4 List<Integer> list = Arrays.asList(3 , 1 , 2 , 3 , 1 ); List<Integer> sorted = new ArrayList <>(new TreeSet <>(list));
适用需求 :排行榜去重排序、统计指标去重后输出有序集合、按权重/分数排序的标签列表。
2. 范围查询 / 取最值(红黑树的优势) 1 2 3 4 5 6 7 8 9 10 11 12 13 treeSet.first(); treeSet.last(); treeSet.lower(3 ); treeSet.higher(3 ); treeSet.floor(3 ); treeSet.ceiling(3 ); treeSet.headSet(3 ); treeSet.tailSet(3 ); treeSet.subSet(1 , 5 ); treeSet.pollFirst(); treeSet.pollLast();
适用需求 :价格区间筛选、时间窗口内数据截取、TopN / 末位淘汰、延迟队列取最早到期任务。
3. 自定义对象排序(必须提供 Comparator 或实现 Comparable) 1 2 3 4 5 TreeSet<User> users = new TreeSet <>(Comparator.comparingInt(User::getAge)); users.add(new User ("张三" , 20 )); users.add(new User ("李四" , 18 ));
⚠️ TreeSet 的「去重」依据是 Comparator/Comparable 的比较结果是否为 0 ,不是 equals!若只按年龄比较,两个同年纪不同名的人会被当作重复。需要严格去重时,Comparator 应把唯一标识也算进去 ,例如 Comparator.comparing(User::getAge).thenComparing(User::getId)。
4. 与 HashSet / LinkedHashSet 选型
需求
选哪个
去重,不关心顺序
HashSet
去重 + 保持插入顺序
LinkedHashSet
去重 + 自动排序 / 范围查询
TreeSet
去重 + 取最大最小 / TopN
TreeSet
💡 注意 :TreeSet 排序成本高于 HashSet,数据量极大且只需去重时优先 HashSet;需要区间、最值、有序输出时再上 TreeSet。
HashMap / LinkedHashMap / TreeMap 常见使用与适用需求 三者都实现 Map 接口(key-value 键值对、key 不允许重复 ),区别只在「遍历顺序」与「底层结构」:
HashMap:基于哈希表 ,无序 (增删查 O(1),日常默认选它)
LinkedHashMap:在 HashMap 基础上用双向链表记录插入/访问顺序 ,可按插入顺序或访问顺序(LRU)遍历
TreeMap:基于红黑树 ,key 自动排序 (自然顺序或 Comparator),增删查 O(log n)
1 2 3 Map<String, Integer> hashMap = new HashMap <>(); Map<String, Integer> linkedMap = new LinkedHashMap <>(); Map<String, Integer> treeMap = new TreeMap <>();
1. 基础增删查改(三者通用) JDK9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 HashMap<String, String> map = new HashMap <>(Map.ofEntries( Map.entry("重庆" , "直辖市" ), Map.entry("四川" , "省会成都" ), Map.entry("广东" , "省会广州" ), Map.entry("江苏" , "省会南京" ), Map.entry("浙江" , "省会杭州" ), Map.entry("山东" , "省会济南" ), Map.entry("河南" , "省会郑州" ), Map.entry("湖北" , "省会武汉" ), Map.entry("湖南" , "省会长沙" ), Map.entry("福建" , "省会福州" ), Map.entry("安徽" , "省会合肥" ) )); map.put("北京" , "首都" ); map.put("上海" , "直辖市" ); map.put("四川" , "天府之国" ); System.out.println(map.get("北京" )); System.out.println(map.get("四川" )); System.out.println(map.size());
JDK8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 HashMap<String, String> map = new HashMap <String, String>(){{ put("a" , "b" ); put("c" , "d" ); }}; System.out.println(map); } Map<String, Integer> map = new HashMap <>(); map.put("a" , 1 ); map.put("b" , 2 ); put修改-》返回原本的值,新增-》返回 null map.get("a" ); map.getOrDefault("c" , 0 ); map.containsKey("a" ); map.containsValue(2 ); map.remove("b" ); map.size(); map.putIfAbsent("a" , 9 ); for (Map.Entry<String, Integer> e : map.entrySet()) { System.out.println(e.getKey() + "=" + e.getValue()); } for (String key : map.keySet()) { System.out.println(key + "=" + map.get(key)); } for (Integer val : map.values()) { System.out.println(val); } map.forEach((k, v) -> System.out.println(k + "=" + v));
2. HashMap 无序 vs LinkedHashMap / TreeMap 有序(核心区别) 1 2 3 4 5 6 7 8 9 Map<String, Integer> h = new HashMap <>(); Map<String, Integer> l = new LinkedHashMap <>(); Map<String, Integer> t = new TreeMap <>(); for (String k : Arrays.asList("c" , "a" , "b" )) { h.put(k, 1 ); l.put(k, 1 ); t.put(k, 1 ); }
适用需求 :
只存键值、不关心顺序(缓存、字典、计数)→ HashMap
需要按放入顺序 输出(导出 Excel 保持列顺序、配置按定义顺序展示)→ LinkedHashMap
需要 key 排序 输出(按字母/数字有序、区间查询)→ TreeMap
3. LinkedHashMap 实现 LRU 缓存(访问顺序) LinkedHashMap 构造时可开启 accessOrder=true,每次 get 会把元素移到队尾,配合 removeEldestEntry 即可实现最近最少使用(LRU)淘汰 。
1 2 3 4 5 6 7 8 9 10 Map<String, Integer> lru = new LinkedHashMap <String, Integer>(16 , 0.75f , true ) { @Override protected boolean removeEldestEntry (Map.Entry<String, Integer> eldest) { return size() > 3 ; } }; lru.put("a" , 1 ); lru.put("b" , 2 ); lru.put("c" , 3 ); lru.get("a" ); lru.put("d" , 4 );
适用需求 :本地缓存、历史记录(保留最近 N 条)、会话/令牌过期淘汰。
4. TreeMap 的范围查询 / 取最值(红黑树优势) 1 2 3 4 5 6 7 8 9 10 11 12 TreeMap<Integer, String> tm = new TreeMap <>(); tm.put(3 , "x" ); tm.put(1 , "y" ); tm.put(2 , "z" ); tm.firstKey(); tm.lastKey(); tm.lowerKey(2 ); tm.higherKey(2 ); tm.floorKey(2 ); tm.ceilingKey(2 ); tm.subMap(1 , 3 ); tm.headMap(2 ); tm.tailMap(2 );
适用需求 :按 key 区间筛选、时间窗口截取、TopN / 末位淘汰、价格/分数区间匹配。
5. 自定义对象做 key(必须正确实现 hashCode + equals) HashMap / LinkedHashMap 用 hashCode + equals 判重,TreeMap 用 Comparator/Comparable 比较。用对象做 key 时务必重写:
1 2 3 4 5 6 7 class User { String id; @Override public int hashCode () { return Objects.hash(id); } @Override public boolean equals (Object o) { return o instanceof User && Objects.equals(id, ((User) o).id); } }
⚠️ key 必须是不可变对象 :放入 Map 后若修改了参与 hashCode/compareTo 的字段,会导致再也查不到该 key。
6. 统计出现次数 / 分组(HashMap 高频场景) 1 2 3 4 5 6 7 8 9 10 11 12 13 Map<String, Integer> count = new HashMap <>(); for (String s : list) { count.put(s, count.getOrDefault(s, 0 ) + 1 ); } list.forEach(s -> count.merge(s, 1 , Integer::sum)); Map<String, List<Order>> byUser = new HashMap <>(); for (Order o : orders) { byUser.computeIfAbsent(o.getUser(), k -> new ArrayList <>()).add(o); }
适用需求 :接口返回分组、报表按维度聚合、词频/访问量统计。
三者选型速查
需求
选哪个
键值存取,不关心顺序
HashMap
按插入顺序 输出
LinkedHashMap
LRU 缓存 / 访问排序淘汰
LinkedHashMap
key自动排序 / 区间查询
TreeMap
计数 / 分组聚合
HashMap
海量数据、只要快
HashMap
💡 记忆法 :要「快」用 HashMap;要「快且按插入/访问顺序」用 LinkedHashMap;要「key 排序 / 区间 / 最值」才上 TreeMap。ConcurrentHashMap 才是并发场景的首选(三者均非线程安全)。
栈(Stack)与 队列(Queue)的区别 两者都是线性数据结构 ,区别仅在「数据的进出顺序(操作端)」不同。
1. 核心区别图示 栈(有底,只开顶口)
1 2 3 4 5 6 ▲ 同一端进、出(LIFO) ┌┐ │3│ ← 顶(最后进,最先出) │2│ │1│ ← 底(最早进,最后出) └┘
队列(没底,两头都开口)
1 2 3 4 ┌───────────┐ │ A │ B │ C │ → 右进(rear) → A 先从左出(front) └───────────┘ 先进先出(FIFO)
2. 对比表格
对比项
栈 Stack
队列 Queue
英文缩写
LIFO(Last In First Out)
FIFO(First In First Out)
操作原则
后进先出
先进先出
操作端
同一端(栈顶)
两端(队尾进、队头出)
入操作
push(压栈)
offer / add(入队)
出操作
pop(弹栈)
poll / remove(出队)
查看操作
peek(只看栈顶)
peek(只看队头)
生活类比
叠盘子(只能从顶部取放)
排队买票(队头走、队尾进)
Java推荐
ArrayDeque(非 Stack)
ArrayDeque / LinkedList
3. 一句话记忆
栈 :只有一个口,”先吃进去的压在底下,最后才能出来”(LIFO)。队列 :有两个口,”先来的先走,后来的排后面”(FIFO)。
4. 操作示意(以 ArrayDeque 为例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.ArrayDeque;ArrayDeque<String> stack = new ArrayDeque <>(); stack.push("1" ); stack.push("2" ); stack.push("3" ); stack.peek(); stack.pop(); ArrayDeque<String> queue = new ArrayDeque <>(); queue.offer("1" ); queue.offer("2" ); queue.offer("3" ); queue.peek(); queue.poll();
⚠️ 注意 :Java 自带的 java.util.Stack 类已过时(继承自 Vector,方法同步、性能差),推荐用 ArrayDeque 实现栈和队列。
泛型(Generics) 泛型是 JDK 5 引入的特性,作用是在编译期做类型检查、避免强制类型转换 ,把”运行时才暴露的类型错误”提前到编译时。核心符号是 <T>(Type 的占位符,T 可换成任意大写字母)。
0. 为什么需要泛型?(强转的痛点) 在泛型出现之前,集合只能存 Object,取出来必须手动强转 ,而且编译期不检查类型 ,错误要到运行时才爆。
1 2 3 4 5 6 7 8 List list = new ArrayList ();list.add("hello" ); list.add(123 ); String s1 = (String) list.get(0 ); String s2 = (String) list.get(1 );
三个痛点 :
强转繁琐 :每次 get 都要 (类型) 强转,代码啰嗦
编译期不检查 :塞错类型编译器无感知,错误延迟到运行时才暴露(生产环境才崩)
类型不安全 :一个 List 里混了多种类型,取的时候根本不知道该转成什么
用了泛型之后 :
1 2 3 4 List<String> list = new ArrayList <>(); list.add("hello" ); String s = list.get(0 );
对比一目了然:泛型把强转和类型检查交给编译器 ,既省掉 (String) 强转,又把 ClassCastException 提前到编译期。
💡 如果故意不写泛型(raw type,如 List list),Java 出于兼容会告警但不报错 ,并默认按 Object 处理,取元素仍需强转——所以新代码务必带泛型。
1. 泛型类(Generic Class) 把类型参数放到类名后面,类内部就可以用这个类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Box <T> { private T value; public void set (T value) { this .value = value; } public T get () { return value; } } Box<String> strBox = new Box <>(); strBox.set("hello" ); String s = strBox.get(); Box<Integer> intBox = new Box <>(); intBox.set(123 ); Integer n = intBox.get();
对比 raw type(需要强转) :如果不用泛型(裸类型),get() 返回 Object,必须手动强转,否则编译报错:
1 2 3 4 Box rawBox = new Box (); rawBox.set("hello" ); String s = (String) rawBox.get();
加了 <String> 之后,编译器记住了”里面装的是 String”,get() 直接返回 String,强转就省掉了——这就是泛型”避免强制类型转换”的含义。
多个类型参数用逗号分隔:class Pair<K, V> { ... }
2. 泛型接口(Generic Interface) 接口也能带类型参数 <T>。实现它时有两种写法,区别只在**”这个 T 到底是谁定”**:
方式一:实现类把 T 定死 (写具体类型,如 String)。以后用这个类,泛型就固定了,想存别的类型就得再写一个新类。
**方式二:实现类自己还带 <T>**(继续留给使用方定)。这个类更通用,用的时候再告诉它 T 是什么。
用一个”只能往里存一个元素的盒子”接口 Holder 来看最直观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public interface Holder <T> { void put (T item) ; T get () ; } public class StringHolder implements Holder <String> { private String value; public void put (String item) { value = item; } public String get () { return value; } } StringHolder h1 = new StringHolder ();h1.put("hello" ); String s = h1.get(); public class AnyHolder <T> implements Holder <T> { private T value; public void put (T item) { value = item; } public T get () { return value; } } AnyHolder<String> h2 = new AnyHolder <>(); h2.put("hello" ); String s2 = h2.get(); AnyHolder<Integer> h3 = new AnyHolder <>(); h3.put(123 ); Integer n = h3.get();
一句话区分:
方式一 = 这个实现”专一”,只服务一种类型(适合固定业务对象)。
方式二 = 这个实现”通用”,谁用谁定类型(适合写工具类)。
💡 日常最常见的泛型接口其实是 Comparator<T>(比较器):new Comparator<String>() { ... } 就是”方式一定死 String”;Collections.sort(list, comparator) 接收 Comparator<? super T> 就是”方式二留 T 给调用方”。
常用泛型接口:List<E>、Set<E>、Map<K,V>、Comparator<T>、Supplier<T> 都是泛型接口。
3. 泛型方法(Generic Method) 类型参数放在方法返回值前面 、修饰符后面。泛型方法不依赖类是否泛型,普通类也能写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public <Q> void log (Q... data) { } a.<String>log("123" , "456" ); a.<String>log("123" , 456 ); --------------------------------------------------- public class GenericMethodDemo { public static <T> T getFirst (List<T> list) { return list.isEmpty() ? null : list.get(0 ); } public static <K, V> V getValue (Map<K, V> map, K key) { return map.get(key); } public static <T extends Number > double sum (List<T> list) { double total = 0 ; for (T t : list) { total += t.doubleValue(); } return total; } } String first = GenericMethodDemo.getFirst(Arrays.asList("a" , "b" ));Integer v = GenericMethodDemo.getValue( Collections.singletonMap("k" , 1 ), "k" );
4. 通配符(Wildcard) 用于”不确定具体类型、只关心范围”的场景,配合 ? 使用: 通配符 ? 只能用在泛型类型 的声明中(如 List<?>、Class<?>),不能单独作为参数的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ? super T 类型是 T 或 T 的父类(下限) public static double sumNumbers (List<? extends Number> list) { double total = 0 ; for (Number n : list) total += n.doubleValue(); return total; } public <T extends Integer > void getData (T data) { System.out.println(data); } getData(123 ); getData(456 );
PECS 原则 (Producer Extends, Consumer Super):
只从集合取 数据(生产者)→ 用 ? extends T
只往集合放 数据(消费者)→ 用 ? super T
5. 泛型常见注意点
注意点
说明 / 示例
不能用基本类型
List<int> ❌ → 必须用 List<Integer>
不能 new 泛型数组
new T[10] ❌(类型擦除限制)
类型擦除
运行时 Box<String> 和 Box<Integer> 都是 Box,泛型信息被擦除
不能 instanceof 泛型
if (obj instanceof Box<String>) ❌ → 只能 obj instanceof Box
静态成员不能用类泛型
private static T x; ❌(静态属于类,泛型属于实例)
泛型方法类型推断
Java 8 起 list.stream().map(x -> ...) 可自动推断
💡 一句话 :泛型 = 编译期类型安全 + 免强转;<T> 是类/接口上的占位符,<T> 在方法返回值前是泛型方法,? extends/super 是通配符上下界。
不可变集合(Immutable Collections)详解(List / Set / Map) 不可变集合一旦创建,内容、长度都不能再改 (增、删、改、排序、clear 全部抛异常),且通常线程安全 。适合做常量配置、方法返回值(防止外部篡改)、多线程共享数据。
1. Java 9+ 原生:List.of / Set.of / Map.of JDK 9 起在接口上直接提供了工厂方法,最简洁,不允许 null 元素 (否则 NullPointerException)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 List<String> list = List.of("a" , "b" , "c" ); list.add("d" ); list.set(0 , "x" ); list.get(0 ); List<String> list = Collections.unmodifiableList(Arrays.asList("a" , "b" , "c" )); Set<Integer> set = Set.of(1 , 2 , 3 ); set.add(4 ); Set<String> set = Collections.unmodifiableSet(new HashSet <>(Arrays.asList("a" , "b" , "c" ))); Map<String, Integer> map = Map.of("a" , 1 , "b" , 2 ); Map<String, Integer> map2 = Map.ofEntries( Map.entry("a" , 1 ), Map.entry("b" , 2 ) ); map.put("c" , 3 ); map.get("a" ); jdk8写法 Map<String, Integer> mutable = new HashMap <>(); mutable.put("a" , 1 ); mutable.put("b" , 2 ); Map<String, Integer> immutable = Collections.unmodifiableMap(mutable);
⚠️ List.of(...) / Set.of(...) / Map.of(...) 不接受 null ;需要”允许 null 的不可变集合”要用 Collections.unmodifiableList(见第 4 点)。
与 Arrays.asList() 的区别 虽然 Arrays.asList() 常用于快速创建列表,但它不等同于 List.of()。
List.of() :返回一个完全不可变 的列表,不支持任何修改操作。
Arrays.asList() :返回一个长度固定但内容可变 的列表。你不能增加或删除元素,但可以替换 已有元素(即调用 set() 方法)。
因此,如果需要在 Java 8 中实现与 List.of() 完全一致的行为,务必使用 Collections.unmodifiableList() 对 Arrays.asList() 的结果进行包装。
如果你在 Java 8 项目中遇到 List.of 或 Set.of 的编译错误,采用上述替代方案即可解决。
2. Collections.unmodifiableXXX(包装已有集合) 把已有可变集合 包一层只读视图。注意:视图只读,但底层原集合仍可改 (改了视图跟着变)。
1 2 3 4 5 6 7 List<String> src = new ArrayList <>(); src.add("a" ); List<String> readOnly = Collections.unmodifiableList(src); readOnly.add("b" ); src.add("c" ); System.out.println(readOnly);
关键点:这是视图(view) 不是副本。要真正冻结数据,应先 new ArrayList<>(src) 拷一份再包装。
3. Guava 的不可变集合(推荐做”真·常量”) Guava 提供 ImmutableList / ImmutableSet / ImmutableMap,创建后完全冻结 (连底层都改不了),且允许 null(会直接报错,更安全)。
1 2 3 4 5 6 7 8 9 10 11 12 ImmutableList<String> list = ImmutableList.of("a" , "b" , "c" ); ImmutableMap<String, Integer> map = ImmutableMap.<String, Integer>builder() .put("a" , 1 ) .put("b" , 2 ) .build(); List<String> temp = Arrays.asList("x" , "y" ); ImmutableList<String> frozen = ImmutableList.copyOf(temp);
Guava 不可变集合是真副本 :改原 temp 不会影响 frozen,是防御外部篡改返回值的最佳选择。
4. 三者对比与选型
方式
语法
真冻结?
允许 null
适用场景
List.of / Set.of / Map.of
JDK9+ 原生
✅ 是
❌ 否
简单常量、少量元素、无 null
Collections.unmodifiableXxx
包装视图
❌ 仅视图
✅ 是
临时只读、需保留原集合可写
ImmutableXxx(Guava)
需引依赖
✅ 是(副本)
❌ 否
返回值防御性拷贝、常量配置、多线程
5. 常见坑
**改了就抛 UnsupportedOperationException**:任何增删改(add/remove/set/clear/put)都会抛,不是静默忽略。
unmodifiableXxx 不是副本 :外层只读、内层可变,原集合一改视图就变——想冻结务必先拷贝。
JDK9 of 系列禁 null :放 null 直接启动即 NPE;需要 null 用 unmodifiable 或 Guava 也不行(Guava 也禁 null),只能自己包。
遍历不可变集合本身可正常读 :只是不能改;遍历、get、contains、size 都正常。
序列化/做返回值 :方法返回不可变集合能让调用方无法破坏内部状态(防御性编程)。
💡 一句话记忆:想要”写死的常量”用 List.of;要”返回给别人但不许他改”用 Guava ImmutableXxx.copyOf;Collections.unmodifiableXxx 只是个只读壳,底层还能动。
Collections 工具类常用 API 说明 java.util.Collections 是操作 Collection(List/Set/Map 等) 的静态工具类,全部 Collections.xxx(...) 调用。下面按「排序 / 查找 / 不可变 / 线程安全 / 其它」分类。
1. 排序与反转(仅 List 适用) 1 2 3 4 5 6 7 List<Integer> list = new ArrayList <>(Arrays.asList(3 , 1 , 2 )); Collections.sort(list); Collections.sort(list, Comparator.reverseOrder()); Collections.reverse(list); Collections.shuffle(list); Collections.rotate(list, 2 );
⚠️ sort/reverse/shuffle/rotate 只作用于 List (有索引),Set/Map 无顺序概念不能用。
2. 查找与极值 1 2 3 4 5 6 7 List<Integer> list = Arrays.asList(3 , 1 , 2 , 5 , 2 ); int max = Collections.max(list); int min = Collections.min(list); int idx = Collections.binarySearch(list, 2 ); int cnt = Collections.frequency(list, 2 );
binarySearch 必须先在已排序 的 List 上用,否则结果无意义。
3. 填充 / 替换 / 拷贝(List) 1 2 3 4 5 6 List<String> list = new ArrayList <>(Collections.nCopies(3 , "x" )); Collections.fill(list, "y" ); Collections.replaceAll(list, "y" , "z" ); Collections.copy(dest, src); Collections.swap(list, 0 , 2 );
Collections.nCopies(n, obj) 生成的 List 不可变 ,常用于 fill 前的初始化。
4. 不可变包装(只读视图) 1 2 3 4 List<String> readOnly = Collections.unmodifiableList(srcList); Set<Integer> roSet = Collections.unmodifiableSet(srcSet); Map<String,Integer> roMap = Collections.unmodifiableMap(srcMap);
这是视图 不是副本:底层原集合仍可改,视图会跟着变(详见上一节”不可变集合”坑点)。
5. 线程安全包装(同步集合) 1 2 3 List<String> syncList = Collections.synchronizedList(new ArrayList <>()); Map<String,Integer> syncMap = Collections.synchronizedMap(new HashMap <>()); Set<Integer> syncSet = Collections.synchronizedSet(new HashSet <>());
包装后每个方法加锁 ,迭代时仍要手动 synchronized;高并发更推荐 java.util.concurrent 包(如 ConcurrentHashMap、CopyOnWriteArrayList)。
6. 单元素 / 空集合 1 2 3 4 List<String> single = Collections.singletonList("only" ); Set<Integer> one = Collections.singleton(9 ); List<String> empty = Collections.emptyList(); Map<String,Integer> emptyMap = Collections.emptyMap();
singletonList / emptyList 返回的也是不可变 集合,add 会抛异常;空集合全局共享同一实例,适合做”无数据时”的返回值。
7. 常用 API 速查表
方法
作用
适用类型
sort(list) / sort(list, cmp)
升序 / 按比较器排序
List
reverse(list)
反转
List
shuffle(list)
随机打乱
List
rotate(list, n)
循环移位
List
max(coll) / min(coll)
最大 / 最小值
Collection
binarySearch(list, k)
二分查找(需先排序)
List
frequency(coll, o)
出现次数
Collection
fill(list, o)
全部填充
List
replaceAll(list, old, new)
替换所有
List
copy(dest, src)
拷贝
List
swap(list, i, j)
交换
List
nCopies(n, o)
生成 n 个副本的不可变 List
—
unmodifiableXxx(x)
只读视图
List/Set/Map
synchronizedXxx(x)
同步(线程安全)包装
List/Set/Map
singleton(o) / singletonList(o)
单元素不可变集合
Set/List
emptyList() / emptySet() / emptyMap()
共享空集合
对应类型
💡 记忆法:Collections 专管”集合级操作”——排序查找(max/min/sort/search)、批量改(fill/replaceAll/copy/swap)、变只读(unmodifiable)、变线程安全(synchronized)、造单/空集合。集合元素的增删改是集合自身 的方法(如 list.add),不属于 Collections。
Stream 流常用 API(项目实战用法) java.util.stream.Stream 是 JDK 8 引入的集合函数式处理管道 :数据像水流一样经过「源 → 中间操作(惰性,返回 Stream)→ 终结操作(触发执行,返回结果)」,链式一行搞定过滤、转换、聚合,代码更短更可读。
核心套路:集合.stream() 开流 → 多个 filter/map 中间操作 → 一个 collect/count/forEach 终结操作收尾。终结操作不调用,中间操作不会执行。
1. 创建 Stream(开流) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 list.stream().forEach(s=>{}) HashMap<String, String> map =new HashMap (); map.put("a" ,"成功" ); 一、 map.keySet().stream().forEach(s=>{}) 二、 map.entrySet().stream().forEach(s=>{}) int [] arr = new int []{1 , 2 , 3 , 4 , 5 , 6 };Arrays.stream(arr).forEach(System.out::println); int [] filtered = Arrays.stream(arr) .filter(n -> n % 2 == 0 ) .toArray(); String[] names = {"Alice" , "Bob" , "Anna" }; String[] result = Arrays.stream(names) .filter(s -> s.startsWith("A" )) .toArray(String[]::new ); List<Student> students = new ArrayList <>(); List<Student> passed = students.stream() .filter(s -> s.getScore() >= 60 ) .collect(Collectors.toList()); ArrayList<Student> passedList = students.stream() .filter(s -> s.getScore() >= 60 ) .collect(Collectors.toCollection(ArrayList::new )); Student[] passedArr = students.stream() .filter(s -> s.getScore() >= 60 ) .toArray(Student[]::new ); List<String> names = students.stream() .filter(s -> s.getScore() >= 60 ) .map(Student::getName) .collect(Collectors.toList()); Stream.of(1 ,2 ,3 ,4 ,5 ,56 ,6 ).forEach Stream.of(引用数据类型数组) OK Stream.of(普通数据类型数组) Fail 如[1 ,2 ,3 ,4 ]
2. filter 过滤(按条件筛元素) 1 2 3 4 List<Integer> nums = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 ); List<Integer> even = nums.stream() .filter(n -> n % 2 == 0 )
4. concat合并流 1 Stream.concat(list1.stream(), list2.stream()).forEach
3. map 转换(元素变形 / 取字段) 1 2 3 4 5 6 7 8 9 10 List<Order> orders = ...; List<String> names = orders.stream() .map(Order::getUserName) .collect(Collectors.toList()); List<Integer> doubled = nums.stream() .map(n -> n * 2 ) .collect(Collectors.toList());
综合 demo:从 [name-age] 字符串数组中取 age,并过滤 age > 50
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 List<String> people = Arrays.asList("xiaoming-22" , "xiaohong-30" , "xiaoli-55" , "xiaowang-62" , "xiaozhao-48" ); List<Integer> ages = people.stream() .map(s -> Integer.parseInt(s.split("-" )[1 ])) .filter(age -> age > 50 ) .collect(Collectors.toList()); List<Integer> ages2 = people.stream() .filter(s -> Integer.parseInt(s.split("-" )[1 ]) > 50 ) .map(s -> Integer.parseInt(s.split("-" )[1 ])) .collect(Collectors.toList()); Map<String, Integer> over50 = people.stream() .filter(s -> Integer.parseInt(s.split("-" )[1 ]) > 50 ) .collect(Collectors.toMap( s -> s.split("-" )[0 ], s -> Integer.parseInt(s.split("-" )[1 ]) )); class Student { private String name; private int age; public Student (String name, int age) { this .name = name; this .age = age; } public int getAge () { return age; } } List<Student> students = people.stream() .map(s -> { String[] parts = s.split("-" ); return new Student (parts[0 ], Integer.parseInt(parts[1 ])); }) .filter(stu -> stu.getAge() > 50 ) .collect(Collectors.toList()); List<Student> students2 = people.stream() .map(s -> new Student (s.split("-" )[0 ], Integer.parseInt(s.split("-" )[1 ]))) .filter(stu -> stu.getAge() > 50 ) .collect(Collectors.toList());
关键点:字符串用 split("-") 拆出 [name, age],[1] 即年龄。推荐**先 filter 再 map**(写法 2),能减少 map 的转换次数;若只要 age 列表用 map 取 [1],若要”名字→年龄”映射用 toMap。
4. flatMap 扁平化(List 套 List 拍平) 1 2 3 4 5 6 7 8 List<List<String>> nested = Arrays.asList( Arrays.asList("a" , "b" ), Arrays.asList("c" ) ); List<String> flat = nested.stream() .flatMap(List::stream) .collect(Collectors.toList());
5. distinct 去重 / sorted 排序 / limit & skip 截取 1 2 3 4 5 6 7 8 9 10 11 12 List<Integer> data = Arrays.asList(3 , 1 , 3 , 2 , 5 , 2 ); data.stream().distinct().collect(Collectors.toList()); data.stream().sorted().collect(Collectors.toList()); data.stream().sorted(Comparator.reverseOrder()).collect(...); data.stream().limit(3 ).collect(Collectors.toList()); data.stream().skip(2 ).collect(Collectors.toList()); list.stream().skip(page * size).limit(size).collect(Collectors.toList()); ArrayList<Integer> arr = new ArrayList <>(Arrays.asList(1 ,2 ,3 ,4 ,5 ,6 )); ArrayList<Integer> collect = arr.stream().limit(3 ).collect(Collectors.toCollection(()->new ArrayList <Integer>()));
6. 聚合终结操作:count / max / min / sum / average 1 2 3 4 5 6 7 long cnt = nums.stream().filter(n -> n > 3 ).count(); Optional<Integer> max = nums.stream().max(Integer::compareTo); Optional<Integer> min = nums.stream().min(Integer::compareTo); int sum = nums.stream().mapToInt(Integer::intValue).sum(); double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0 );
7. collect 收集(转集合 / 分组 / 统计).toArray 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 String[] list = list.stream().toArray(s->new String [s]) List<String> l = list.stream().collect(Collectors.toList()); Set<String> st = list.stream().collect(Collectors.toSet()); Map<String, Order> map = orders.stream() .collect(Collectors.toMap(Order::getId, o -> o)); Map<String, List<Order>> byUser = orders.stream() .collect(Collectors.groupingBy(Order::getUserName)); Map<String, Long> countByUser = orders.stream() .collect(Collectors.groupingBy(Order::getUserName, Collectors.counting())); IntSummaryStatistics stat = nums.stream().mapToInt(Integer::intValue).summaryStatistics();stat.getSum(); stat.getAverage(); stat.getMax(); stat.getMin(); stat.getCount();
8. forEach 遍历 / anyMatch & allMatch & noneMatch 判断 1 2 3 4 5 list.stream().forEach(System.out::println); boolean hasEven = nums.stream().anyMatch(n -> n % 2 == 0 ); boolean allPos = nums.stream().allMatch(n -> n > 0 ); boolean noneNeg = nums.stream().noneMatch(n -> n < 0 );
9. reduce 归约(自定义累积,如连字符串 / 累乘) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 List<Integer> numbers = Arrays.asList(3 , 5 , 1 , 9 ); Optional<Integer> max = numbers.stream().reduce(Integer::max); max.ifPresent(System.out::println); Optional<Integer> product = numbers.stream().reduce((a, b) -> a * b); System.out.println(product.orElse(1 )); List<Integer> numbers = Arrays.asList(1 , 2 , 3 , 4 ); int sum = numbers.stream().reduce(0 , Integer::sum);System.out.println(sum); List<String> words = Arrays.asList("Java" , "Stream" , "Reduce" ); String result = words.stream().reduce("结果: " , (a, b) -> a + " | " + b);System.out.println(result); List<String> words = Arrays.asList("Java" , "Stream" , "Reduce" ); String result1 = words.stream().reduce("结果: " , (a, b) -> a + " | " + b); Optional<String> result2 = words.stream().reduce((a, b) -> a + " | " + b); System.out.println(result1); System.out.println(result2.get());
10. 常用 API 速查表 + 避坑
分类
API
说明
中间(惰性)
filter
条件过滤
中间
map
元素一对一转换
中间
flatMap
多维拍平为一维
中间
distinct / sorted
去重 / 排序
中间
limit / skip
截取前 n / 跳过前 n(分页)
中间
peek
窥视(调试用,不改元素)
终结
collect
收集成 List/Set/Map/分组
终结
count / sum / max / min
聚合统计
终结
forEach
遍历执行
终结
anyMatch / allMatch / noneMatch
条件判断
终结
reduce
自定义归约
终结
findFirst / findAny
取首个 / 任一个(返回 Optional)
⚠️ 避坑 :
Stream 只能消费一次 :调用过终结操作后不能再复用,要重算就重新 stream()。
forEach 里改外部变量要用 final 或原子类 (lambda 捕获的局部变量必须 effectively final),否则编译报错。
**toMap 的 key 重复会抛 IllegalStateException**:重复时用 (oldV, newV) -> newV 指定覆盖策略。
终结操作才触发执行 :只写 filter/map 不写 collect/count 等,什么都不会发生。
并行流 parallelStream() 不是越快越好 :小数据量反而更慢,且线程安全问题要自己保证。
💡 记忆法:Stream = 开流(stream())→ 中间操作(filter/map/sorted,可链式)→ 终结操作(collect/count/forEach,收尾) 。项目里最常用的是 filter + map + collect、groupingBy 分组统计、mapToInt().sum() 求和。
方法引用(Method Reference)说明(由浅入深) 方法引用是 JDK 8 引入的语法,写法里有 ::,作用是:把一个已经存在的方法,直接当成 Lambda 使用 。
它不是新功能,本质是 Lambda 的简写 。当 Lambda 的方法体只是”调用某个现成方法”时,就可以改成方法引用。
1 2 3 4 5 6 7 List<String> list = Arrays.asList("a" , "b" , "c" ); list.forEach(s -> System.out.println(s)); list.forEach(System.out::println);
核心记忆:方法引用 = 省略 Lambda 参数 + 省略方法调用过程,只留下”谁的方法”。
1. 先理解:方法引用为什么能替代 Lambda 方法引用必须依赖 函数式接口 (只有一个抽象方法的接口),例如:
函数式接口
抽象方法
常见场景
Consumer<T>
void accept(T t)
forEach 消费数据
Function<T,R>
R apply(T t)
map 转换数据
Predicate<T>
boolean test(T t)
filter 条件判断
Supplier<T>
T get()
延迟创建对象
Comparator<T>
int compare(T a, T b)
排序
BiFunction<T,U,R>
R apply(T t, U u)
两个参数转一个结果
只要方法引用的参数、返回值能和函数式接口对上,编译器就能自动匹配。
1 2 3 4 5 6 7 8 9 list.forEach(System.out::println); List<Integer> nums = Arrays.asList("1" , "2" , "3" ).stream() .map(Integer::parseInt) .collect(Collectors.toList());
2. 从 Lambda 改成方法引用的判断标准 如果 Lambda 只是原样调用一个现成方法 ,就可以改成方法引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 s -> System.out.println(s) System.out::println s -> Integer.parseInt(s) Integer::parseInt s -> s.length() String::length s -> System.out.println(s.toUpperCase()) s -> { String v = s.trim(); System.out.println(v); }
判断口诀:Lambda 里只有一个方法调用,并且参数没被加工,就优先考虑方法引用。
3. 五种常见写法总览
类型
语法
等价 Lambda
常见示例
静态方法引用
类名::静态方法
x -> 类名.静态方法(x)
Integer::parseInt、String::valueOf
指定对象的实例方法引用
对象::实例方法
x -> 对象.实例方法(x)
System.out::println、log::info
任意对象的实例方法引用
类名::实例方法
x -> x.实例方法()
String::length、String::trim
构造方法引用
类名::new
x -> new 类名(x)
User::new、ArrayList::new
数组构造引用
类型[]::new
n -> new 类型[n]
String[]::new、User[]::new
4. 静态方法引用:类名::静态方法 适合引用工具类静态方法,如 Integer.parseInt、String.valueOf、Objects.nonNull、Objects.isNull。
1 2 3 4 5 6 7 8 9 10 11 List<String> list = Arrays.asList("1" , "2" , "3" ); List<Integer> nums1 = list.stream() .map(s -> Integer.parseInt(s)) .collect(Collectors.toList()); List<Integer> nums2 = list.stream() .map(Integer::parseInt) .collect(Collectors.toList());
常见开发写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 List<Object> values = Arrays.asList("a" , 1 , true ); List<String> strList = values.stream() .map(String::valueOf) .collect(Collectors.toList()); List<String> names = Arrays.asList("tom" , null , "jack" ).stream() .filter(Objects::nonNull) .collect(Collectors.toList()); List<String> nullList = Arrays.asList("tom" , null , "jack" ).stream() .filter(Objects::isNull) .collect(Collectors.toList()); Integer total = Arrays.asList(1 , 2 , 3 ).stream() .reduce(0 , Integer::sum);
5. 指定对象的实例方法引用:对象::实例方法 这种写法中,:: 左边是一个已经存在的对象 ,不是类名。
1 2 3 4 5 6 7 List<String> list = Arrays.asList("a" , "b" , "c" ); list.forEach(s -> System.out.println(s)); list.forEach(System.out::println);
项目中常见写法:
1 2 3 4 5 6 7 8 9 StringBuilder sb = new StringBuilder ();Arrays.asList("A" , "B" , "C" ).forEach(sb::append); System.out.println(sb.toString()); List<String> result = new ArrayList <>(); Arrays.asList("x" , "y" ).forEach(result::add);
注意:对象::方法 的对象是固定的,比如 System.out、sb、result,参数只会传给这个对象的方法。
6. 任意对象的实例方法引用:类名::实例方法 这是最容易混淆的一种。它的意思是:把 Lambda 的第一个参数当成方法调用者 。
1 2 3 4 5 6 7 8 9 10 11 List<String> list = Arrays.asList(" apple " , " banana " , " pear " ); List<String> trim1 = list.stream() .map(s -> s.trim()) .collect(Collectors.toList()); List<String> trim2 = list.stream() .map(String::trim) .collect(Collectors.toList());
更多常用写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 List<String> words = Arrays.asList("banana" , "apple" , "pear" ); List<Integer> lenList = words.stream() .map(String::length) .collect(Collectors.toList()); List<String> upperList = words.stream() .map(String::toUpperCase) .collect(Collectors.toList()); words.sort(String::compareToIgnoreCase);
类名::实例方法 的参数对应关系:
1 2 3 4 5 6 7 String::length String::compareToIgnoreCase
关键点:**String::length 不是调用静态方法,而是让传进来的 String 对象自己调 length()。**
7. 构造方法引用:类名::new 当 Lambda 的作用只是 new 一个对象时,可以用构造方法引用。
1 2 3 4 5 6 Supplier<List<String>> supplier1 = () -> new ArrayList <>(); Supplier<List<String>> supplier2 = ArrayList::new ; List<String> list = supplier2.get(); list.add("a" );
有参构造示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class User { private Integer id; private String name; public User (Integer id) { this .id = id; } public User (String name) { this .name = name; } } List<Integer> ids = Arrays.asList(1 , 2 , 3 ); List<User> users1 = ids.stream() .map(id -> new User (id)) .collect(Collectors.toList()); List<User> users2 = ids.stream() .map(User::new ) .collect(Collectors.toList());
两个参数构造示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class OrderDTO { private String orderNo; private BigDecimal amount; public OrderDTO (String orderNo, BigDecimal amount) { this .orderNo = orderNo; this .amount = amount; } } BiFunction<String, BigDecimal, OrderDTO> creator1 = (orderNo, amount) -> new OrderDTO (orderNo, amount); BiFunction<String, BigDecimal, OrderDTO> creator2 = OrderDTO::new ; OrderDTO order = creator2.apply("RPT-202608250001" , new BigDecimal ("199.99" ));
这个例子更容易看出参数匹配关系:BiFunction<String, BigDecimal, OrderDTO> 的前两个泛型分别对应构造方法参数 String orderNo、BigDecimal amount,第三个泛型 OrderDTO 对应构造方法返回的新对象。
构造方法引用能不能写,取决于目标函数式接口的参数列表能否匹配对应构造方法。
8. 数组构造引用:类型[]::new 最常见于 Stream 的 toArray,用于生成指定类型数组。
1 2 3 4 5 6 7 List<String> list = Arrays.asList("a" , "b" , "c" ); Object[] arr1 = list.stream().toArray(); String[] arr2 = list.stream().toArray(String[]::new );
对象数组也一样:
1 2 List<User> userList = new ArrayList <>(); User[] users = userList.stream().toArray(User[]::new );
9. this::方法 和 super::方法 当前类的方法也可以用方法引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DemoService { public void print (String s) { System.out.println(s); } public void test () { List<String> list = Arrays.asList("a" , "b" ); list.forEach(s -> this .print(s)); list.forEach(this ::print); } }
父类方法引用:
1 2 3 4 5 6 7 8 9 10 11 class Parent { public void print (String s) { System.out.println("parent: " + s); } } class Child extends Parent { public void test () { Arrays.asList("a" , "b" ).forEach(super ::print); } }
自定义类方法引用示例:奇数、偶数筛选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class NumberFilter { public static boolean isOdd (Integer num) { return num != null && num % 2 != 0 ; } public static boolean isEven (Integer num) { return num != null && num % 2 == 0 ; } } List<Integer> nums = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 , null ); List<Integer> oddList1 = nums.stream() .filter(num -> NumberFilter.isOdd(num)) .collect(Collectors.toList()); List<Integer> oddList2 = nums.stream() .filter(NumberFilter::isOdd) .collect(Collectors.toList()); List<Integer> evenList = nums.stream() .filter(NumberFilter::isEven) .collect(Collectors.toList());
关键点:filter 需要的是 Predicate<Integer>,它的抽象方法是 boolean test(Integer num);而 NumberFilter.isOdd(Integer num) / NumberFilter.isEven(Integer num) 都是接收 Integer、返回 boolean,所以可以直接写成 NumberFilter::isOdd / NumberFilter::isEven。
如果判断方法不是静态方法,也可以用”对象::实例方法”:
1 2 3 4 5 6 7 8 9 10 11 class NumberChecker { public boolean isEven (Integer num) { return num != null && num % 2 == 0 ; } } NumberChecker checker = new NumberChecker ();List<Integer> evenList = nums.stream() .filter(checker::isEven) .collect(Collectors.toList());
10. 在 Stream 中最常见的开发写法 10.1 map 取字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Order { private String orderNo; private BigDecimal amount; public String getOrderNo () { return orderNo; } public BigDecimal getAmount () { return amount; } } List<Order> orders = new ArrayList <>(); List<String> orderNos1 = orders.stream() .map(o -> o.getOrderNo()) .collect(Collectors.toList()); List<String> orderNos2 = orders.stream() .map(Order::getOrderNo) .collect(Collectors.toList());
10.2 filter 调判断方法 1 2 3 4 5 6 7 8 9 10 11 12 13 class User { private Boolean active; public boolean isActive () { return Boolean.TRUE.equals(active); } } List<User> users = new ArrayList <>(); List<User> activeUsers = users.stream() .filter(User::isActive) .collect(Collectors.toList());
10.3 sorted 排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 List<Order> sortedOrders = orders.stream() .sorted(Comparator.comparing(Order::getAmount)) .collect(Collectors.toList()); List<Order> descOrders = orders.stream() .sorted(Comparator.comparing(Order::getAmount).reversed()) .collect(Collectors.toList()); List<Order> multiSorted = orders.stream() .sorted(Comparator.comparing(Order::getAmount) .thenComparing(Order::getOrderNo)) .collect(Collectors.toList());
10.4 toMap 收集成 Map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Map<String, Order> orderMap = orders.stream() .collect(Collectors.toMap(Order::getOrderNo, Function.identity())); Map<String, BigDecimal> amountMap = orders.stream() .collect(Collectors.toMap(Order::getOrderNo, Order::getAmount)); Map<String, Order> noDuplicateMap = orders.stream() .collect(Collectors.toMap( Order::getOrderNo, Function.identity(), (oldValue, newValue) -> newValue ));
Function.identity() 等价于 x -> x,表示 value 就是当前对象本身。
10.5 groupingBy 分组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Employee { private String deptName; private String userName; public String getDeptName () { return deptName; } public String getUserName () { return userName; } } List<Employee> employees = new ArrayList <>(); Map<String, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDeptName)); Map<String, Long> countByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDeptName, Collectors.counting())); Map<String, List<String>> namesByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDeptName, Collectors.mapping(Employee::getUserName, Collectors.toList()) ));
10.6 mapToInt / mapToDouble / reduce 聚合 1 2 3 4 5 6 7 8 9 List<String> nums = Arrays.asList("1" , "2" , "3" ); int sum = nums.stream() .map(Integer::parseInt) .reduce(0 , Integer::sum); int totalLength = Arrays.asList("aa" , "bbb" , "c" ).stream() .mapToInt(String::length) .sum();
11. 在 Optional 中的常见写法 1 2 3 4 5 6 7 8 9 10 Optional<String> nameOpt = Optional.of("xiaoming" ); nameOpt.ifPresent(System.out::println); Optional<Integer> lenOpt = nameOpt.map(String::length); String name = Optional.<String>empty().orElseGet(() -> "default" );
如果默认值创建逻辑已有方法,可以写方法引用:
1 2 3 4 5 6 7 class DefaultNameFactory { public static String create () { return "default" ; } } String name = Optional.<String>empty().orElseGet(DefaultNameFactory::create);
12. 普通字符串数据处理示例:name-age 结合前面 Stream 示例,普通数组也可以使用方法引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<String> people = Arrays.asList("xiaoming-22" , "xiaohong-30" , "xiaoli-55" , "xiaowang-62" ); List<Integer> ages = people.stream() .map(s -> s.split("-" )[1 ]) .map(Integer::parseInt) .filter(age -> age > 50 ) .collect(Collectors.toList()); Map<String, Integer> ageMap = people.stream() .filter(s -> Integer.parseInt(s.split("-" )[1 ]) > 50 ) .collect(Collectors.toMap( s -> s.split("-" )[0 ], s -> Integer.parseInt(s.split("-" )[1 ]) ));
这里 Integer::parseInt 是典型的静态方法引用。
13. 方法引用和 Lambda 的选择建议
场景
推荐写法
原因
只是调用现成方法
方法引用
简洁,可读性高
需要额外加工参数
Lambda
方法引用无法表达中间逻辑
有多条语句
Lambda
方法引用只能指向一个方法
方法名语义清楚
方法引用
如 Order::getAmount 一眼看懂
方法引用导致歧义
Lambda
显式写参数更清楚
初学者看不懂复杂引用
Lambda
可读性优先
1 2 3 4 5 6 7 orders.stream().map(Order::getOrderNo).collect(Collectors.toList()); orders.stream() .map(o -> "NO-" + o.getOrderNo()) .collect(Collectors.toList());
14. 常见坑与解决办法 14.1 签名必须匹配 1 2 3 4 5 Stream.of("1" , "2" ).map(Integer::parseInt);
14.2 重载方法可能歧义 1 2 3 4 5 6 7 8 9 10 11 12 class Printer { public void print (String s) {} public void print (Integer i) {} } Printer printer = new Printer ();Consumer<String> c1 = printer::print; Consumer<String> c2 = s -> printer.print(s);
14.3 类名::实例方法 不是静态方法调用 1 2 3 4 5 6 String::length String::compareToIgnoreCase
14.4 toMap 的 key 重复会报错 1 2 3 4 5 6 7 8 9 10 11 Map<String, Order> map = orders.stream() .collect(Collectors.toMap(Order::getOrderNo, Function.identity())); Map<String, Order> safeMap = orders.stream() .collect(Collectors.toMap( Order::getOrderNo, Function.identity(), (oldValue, newValue) -> newValue ));
14.5 不要为了方法引用牺牲可读性 1 2 3 4 5 6 7 list.stream().map(String::trim).filter(StringUtils::isNotBlank).collect(Collectors.toList()); list.stream() .filter(s -> s != null && s.trim().length() > 3 ) .collect(Collectors.toList());
15. 常用速查表
需求
Lambda
方法引用
打印每个元素
x -> System.out.println(x)
System.out::println
字符串转数字
s -> Integer.parseInt(s)
Integer::parseInt
对象转字符串
x -> String.valueOf(x)
String::valueOf
过滤非空
x -> Objects.nonNull(x)
Objects::nonNull
取字符串长度
s -> s.length()
String::length
去空格
s -> s.trim()
String::trim
转大写
s -> s.toUpperCase()
String::toUpperCase
忽略大小写比较
(a,b) -> a.compareToIgnoreCase(b)
String::compareToIgnoreCase
取字段
o -> o.getOrderNo()
Order::getOrderNo
判断状态
u -> u.isActive()
User::isActive
求和
(a,b) -> Integer.sum(a,b)
Integer::sum
创建对象
id -> new User(id)
User::new
创建集合
() -> new ArrayList<>()
ArrayList::new
创建数组
n -> new String[n]
String[]::new
最后记忆:**:: 左边如果是对象,就是固定对象调用方法;左边如果是类,要看右边方法是不是静态方法。静态方法就是普通调用;实例方法则是”把第一个参数当调用者”。**
IO 流分类总览(基础流 / 包装流) 先建立全局认知再学各个流:Java IO 的流分两大类——基础流 (直接连文件)和包装流 (套在别的流外面增强功能,装饰器模式)。File 不是流 ,它只是文件/目录的抽象,管元信息(路径/大小/存在/创建/删除),不负责内容读写;读写内容必须用流去「打开」它。
1. 基础流(直接连文件,共 4 个,所有组装链的底层)
类
方向
单位
用处
FileInputStream
读
字节
读二进制 文件(图片/音视频/压缩包/任意文件)的原始字节
FileOutputStream
写
字节
写原始字节到文件(啥文件都能写,配合包装流写文本更方便)
FileReader
读
字符
读文本 文件(内部 = InputStreamReader + FileInputStream 的预制组合,用平台默认编码,不能指定编码 )
FileWriter
写
字符
写文本 文件(内部 = OutputStreamWriter + FileOutputStream 的预制组合,同样不能指定编码)
2. 包装流(套在别的流外面,按功能分 5 类)
类别
类
用处
缓冲流 (提速)
BufferedInputStream / BufferedOutputStream
包装任意字节流 ,内置 8KB 缓冲区,减少磁盘调用次数,读写大文件提速明显
BufferedReader / BufferedWriter
包装任意字符流 ,除了缓冲提速,还提供 readLine() 按行读、newLine() 平台无关换行——处理文本首选
转换流 (字节↔字符桥梁)
InputStreamReader
把字节输入流 转成字符输入流 (byte→char),可显式指定编码 ,读 GBK 等非 UTF-8 文件防乱码的唯一可靠手段
OutputStreamWriter
把字符输出流 转成字节输出流 (char→byte),可指定编码写出
压缩流 (解压/压缩)
GZIPInputStream / GZIPOutputStream
包装字节流,读/写**.gz** 单文件压缩格式,边读边解压、边写边压缩
ZipInputStream / ZipOutputStream
读/写**.zip** 压缩包(可含多个条目),配合 ZipEntry 遍历/添加压缩包内文件
对象流 (序列化)
ObjectInputStream / ObjectOutputStream
直接读写 Java 对象(序列化/反序列化),对象类需实现 Serializable
打印流 (便捷输出)
PrintStream / PrintWriter
提供 println() / print() / printf() 直接打印各种类型;System.out 就是 PrintStream
3. 组装规则与典型链 组装规则:基础流打底,包装流按需层层套 (转换流只能包字节流;缓冲流建议永远套一层)。
1 2 3 4 5 6 7 8 9 10 普通文本: File → FileInputStream → InputStreamReader → BufferedReader → 程序 (便捷写法: File → FileReader → BufferedReader,但无法指定编码) GBK 文本: File → FileInputStream → InputStreamReader("GBK") → BufferedReader gzip 文本: File → FileInputStream → GZIPInputStream → InputStreamReader → BufferedReader 二进制: File → FileInputStream → BufferedInputStream → 程序 对象序列化: File → FileOutputStream → ObjectOutputStream → writeObject(user)
要点 :
File 不是流 :只描述文件本身(在哪、多大、存在否),读写内容必须靠流打开它
基础流只有 4 个 :字节 2 个(FileInput/OutputStream)+ 字符 2 个(FileReader/Writer),直连文件;其余全是包装流
包装流可自由叠加 :如 BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(file)))) 一条链读 gzip 文本
转换流是唯一桥梁 :字节流 ↔ 字符流只能经 InputStreamReader/OutputStreamWriter;FileReader/FileWriter 只是它的预制组合,不能指定编码
只需关闭最外层包装流 ,其 close() 会级联关闭内部被包装的流
File 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import java.io.File;import java.io.IOException;import java.nio.file.*;File file = new File ("D:/data/test.txt" );File dir = new File ("D:/data/subdir" );file.exists(); file.isFile(); file.isDirectory(); file.getName(); file.getPath(); file.getAbsolutePath(); file.getParent(); file.length(); file.createNewFile(); dir.mkdir(); dir.mkdirs(); file.delete(); file.renameTo(new File ("D:/data/renamed.txt" )); File parent = new File ("D:/data" );String[] names = parent.list(); File[] files = parent.listFiles(); File[] txts = parent.listFiles((d, name) -> name.endsWith(".txt" )); Path path = Paths.get("D:/data/test.txt" );Path dir2 = Paths.get("D:/data/subdir" );Files.exists(path); Files.isRegularFile(path); Files.isDirectory(dir2); Files.createFile(path); Files.createDirectory(dir2); Files.createDirectories(dir2); String content = Files.readString(path); List<String> lines = Files.readAllLines(path); byte [] bytes = Files.readAllBytes(path); Files.writeString(path, "hello" , StandardOpenOption.CREATE); Files.write(path, lines); Files.write(path, bytes); Files.copy(path, Paths.get("D:/backup/test.txt" ), StandardCopyOption.REPLACE_EXISTING); Files.move(path, Paths.get("D:/data/moved.txt" ), StandardCopyOption.REPLACE_EXISTING); Files.delete(path); Files.deleteIfExists(path); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir2, "*.txt" )) { for (Path entry : stream) { System.out.println(entry.getFileName()); } } Files.walk(Paths.get("D:/data" )) .filter(Files::isRegularFile) .forEach(System.out::println); long size = Files.size(path); FileTime time = Files.getLastModifiedTime(path);
File vs NIO.2 :java.io.File 是老 API,操作失败只返回 false,不告诉原因;java.nio.file 的 Files 工具类会抛明确异常,且支持原子操作、符号链接、文件属性等,新代码优先用 NIO.2 。
FileOutputStream(字节输出流) FileOutputStream 用于向文件写入字节 (图片、压缩包、二进制等),写文本需手动转字节;纯文本推荐用 FileWriter 或 NIO.2 的 Files.write。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import java.io.FileOutputStream;import java.io.IOException;try (FileOutputStream fos = new FileOutputStream ("D:/data/out.bin" )) { fos.write(65 ); fos.write(new byte []{66 , 67 }); String s = "hello" ; fos.write(s.getBytes()); fos.write(s.getBytes("UTF-8" )); fos.close(); } catch (IOException e) { e.printStackTrace(); } try (FileOutputStream fos = new FileOutputStream ("D:/data/out.bin" , true )) { fos.write("追加内容" .getBytes("UTF-8" )); fos.close(); } catch (IOException e) { e.printStackTrace(); } try (FileOutputStream fos = new FileOutputStream ("D:/data/out.bin" )) { fos.getChannel().position(1024 ); fos.write(new byte []{1 , 2 , 3 }); } try (FileInputStream fis = new FileInputStream ("D:/data/src.bin" ); FileOutputStream fos = new FileOutputStream ("D:/data/dest.bin" )) { byte [] buf = new byte [8192 ]; int len; while ((len = fis.read(buf)) != -1 ) { fos.write(buf, 0 , len); } }
要点 :
用 try-with-resources 自动关闭流(close 内含 flush),避免资源泄漏;示例 1/2/3 均已采用
write(byte[], off, len) 比单字节 write(int) 效率高得多,大文件务必用缓冲
文本场景优先 Files.writeString(path, str)(NIO.2),二进制/流式场景才用 FileOutputStream
FileOutputStream 构造时文件已存在会被覆盖 (除非 append=true),不会抛异常
FileInputStream 用于从文件读取字节 (图片、压缩包、二进制等);读文本需手动转字符。纯文本推荐 Files.readString。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import java.io.FileInputStream;import java.io.IOException;try (FileInputStream fis = new FileInputStream ("D:/data/in.bin" )) { int b; while ((b = fis.read()) != -1 ) { } } try (FileInputStream fis = new FileInputStream ("D:/data/in.bin" )) { byte [] buf = new byte [8192 ]; int len; while ((len = fis.read(buf)) != -1 ) { sout(new String (buf, 0 ,len)) } } try (FileInputStream fis = new FileInputStream ("D:/data/in.bin" )) { byte [] all = fis.readAllBytes(); String text = new String (all, "UTF-8" ); } try (FileInputStream fis = new FileInputStream ("D:/data/in.bin" )) { fis.skip(1024 ); fis.getChannel().position(2048L ); byte [] buf = new byte [1024 ]; int len = fis.read(buf); }
要点 :
用 try-with-resources 自动关闭流,避免资源泄漏
务必用带 byte[] 缓冲的 read(buf),不要 用单字节 read(),大文件性能差距巨大
read() / read(buf) 返回 -1 表示已读到文件末尾(EOF)
读文本必须手动 new String(bytes, charset) 指定编码,否则用平台默认编码易乱码
大文件避免 readAllBytes(),会一次性撑爆堆内存
字符输入流(FileReader / BufferedReader) FileInputStream 读文本要手动按编码转字符,容易乱码;字符流 以「字符」为单位读写,内部按 Charset 自动编解码,专用于文本文件。
1. FileReader(直接读文本) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.FileReader;import java.io.IOException;import java.nio.charset.StandardCharsets;try (FileReader fr = new FileReader ("D:/data/in.txt" , StandardCharsets.UTF_8)) { int c; StringBuilder sb = new StringBuilder (); while ((c = fr.read()) != -1 ) { sb.append((char ) c); } System.out.println("逐字符读取内容:" + sb); } try (FileReader fr = new FileReader ("D:/data/in.txt" , StandardCharsets.UTF_8)) { char [] buf = new char [8192 ]; int len; StringBuilder sb = new StringBuilder (); while ((len = fr.read(buf)) != -1 ) { sb.append(buf, 0 , len); } System.out.println("批量读取内容(共 " + sb.length() + " 个字符):" + sb); }
2. BufferedReader(按行读取,性能更好) 1 2 3 4 5 6 7 8 9 10 11 12 import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedReader br = new BufferedReader ( new FileReader ("D:/data/in.txt" , StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.io.BufferedReader;import java.io.FileInputStream;import java.io.InputStreamReader;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedReader br = new BufferedReader ( new InputStreamReader (new FileInputStream ("D:/data/in.txt" ), "GBK" ))) { String line; while ((line = br.readLine()) != null ) { } }
4. 文本文件拷贝:边读边写(BufferedReader + BufferedWriter) 核心思路与字节流拷贝一致:read 读出一块字符,立刻 write 写出去,循环到 EOF。字符流额外支持「按行」拷贝,更贴近文本处理习惯。仅用于文本文件 ,二进制文件请用字节流拷贝(否则会按编码重新解码/编码导致损坏)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedReader br = new BufferedReader ( new FileReader ("D:/data/src.txt" , StandardCharsets.UTF_8)); BufferedWriter bw = new BufferedWriter ( new FileWriter ("D:/data/dest.txt" , StandardCharsets.UTF_8))) { char [] buf = new char [8192 ]; int len; while ((len = br.read(buf)) != -1 ) { bw.write(buf, 0 , len); } } try (BufferedReader br = new BufferedReader ( new FileReader ("D:/data/src.txt" , StandardCharsets.UTF_8)); BufferedWriter bw = new BufferedWriter ( new FileWriter ("D:/data/dest.txt" , StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) { bw.write(line); bw.newLine(); } }
要点 :
读文本优先 BufferedReader + FileReader(或指定编码的 InputStreamReader),按行读取最省心
非 UTF-8 文件务必通过 InputStreamReader 显式指定编码,否则平台默认编码易乱码
始终用 try-with-resources 自动关闭流
read() / read(buf) / readLine() 返回 -1 / null 表示已到末尾(EOF)
字符输出流(FileWriter / BufferedWriter) 字符输出流以「字符」为单位写出,内部按 Charset 把字符编码为字节,专用于写文本文件。
1. FileWriter(直接写文本) 1 2 3 4 5 6 7 8 9 10 11 import java.io.FileWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;try (FileWriter fw = new FileWriter ("D:/data/out.txt" , StandardCharsets.UTF_8, false )) { fw.write("你好,世界" ); fw.write("\n" ); fw.append("追加内容" ); fw.flush(); }
2. BufferedWriter(按行写出,性能更好) 1 2 3 4 5 6 7 8 9 10 11 12 import java.io.BufferedWriter;import java.io.FileWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedWriter bw = new BufferedWriter ( new FileWriter ("D:/data/out.txt" , StandardCharsets.UTF_8))) { bw.write("第一行" ); bw.newLine(); bw.write("第二行" ); bw.flush(); }
3. OutputStreamWriter(字符流 → 字节流桥梁,可指定编码) 1 2 3 4 5 6 7 8 9 10 11 12 import java.io.BufferedWriter;import java.io.FileOutputStream;import java.io.OutputStreamWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedWriter bw = new BufferedWriter ( new OutputStreamWriter (new FileOutputStream ("D:/data/out.txt" ), "GBK" ))) { bw.write("GBK 编码的中文" ); bw.newLine(); }
要点 :
写文本优先 BufferedWriter + FileWriter,newLine() 写平台无关换行
非 UTF-8 文件用 OutputStreamWriter 显式指定编码
写完调用 flush()(或依赖 try-with-resources 自动关闭时 flush)确保数据落盘
FileWriter(path, true) 追加,FileWriter(path) 覆盖,注意别误覆盖
字节流 vs 字符流 对照表
维度
字节流(InputStream / OutputStream)
字符流(Reader / Writer)
处理单位
1 字节
1 字符(按编码 1~n 字节)
典型类
FileInputStream / FileOutputStream
FileReader / FileWriter
适用场景
图片、音视频、压缩包等二进制
文本文件(需正确编码)
编码处理
不处理,原样字节
自动按 Charset 编解码
桥接类
—
InputStreamReader / OutputStreamWriter
缓冲包装
BufferedInputStream / BufferedOutputStream
BufferedReader / BufferedWriter
四个 Buffered* 类都是装饰器 :包装在基础流(FileInputStream / FileOutputStream / FileReader / FileWriter)外面,内部维护一块默认 8KB 的缓冲区,把「每次 1 字节/1 字符的底层调用」攒成「一批一拨」,大幅减少磁盘/系统调用次数,性能提升明显。实际开发中基础流几乎总是要套一层缓冲流 。
1 2 3 4 5 6 ┌─────────────────────────── 装饰器模式 ───────────────────────────┐ │ 字节: new BufferedInputStream( new FileInputStream(...) ) │ │ 字节: new BufferedOutputStream( new FileOutputStream(...) ) │ │ 字符: new BufferedReader( new FileReader(...) ) │ │ 字符: new BufferedWriter( new FileWriter(...) ) │ └──────────────────────────────────────────────────────────────────┘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.io.BufferedInputStream;import java.io.FileInputStream;import java.io.IOException;try (BufferedInputStream bis = new BufferedInputStream ( new FileInputStream ("D:/data/in.bin" ), 8192 )) { int b; while ((b = bis.read()) != -1 ) { } byte [] buf = new byte [8192 ]; int len; while ((len = bis.read(buf)) != -1 ) { } bis.mark(1024 ); int first = bis.read(); bis.reset(); int first2 = bis.read(); }
2. BufferedOutputStream(缓冲字节输出流) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.io.BufferedOutputStream;import java.io.FileOutputStream;import java.io.IOException;try (BufferedOutputStream bos = new BufferedOutputStream ( new FileOutputStream ("D:/data/out.bin" ))) { bos.write("hello" .getBytes("UTF-8" )); bos.write(65 ); bos.flush(); }
3. BufferedReader(缓冲字符输入流) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedReader br = new BufferedReader ( new FileReader ("D:/data/in.txt" , StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) { } char [] buf = new char [8192 ]; int len; while ((len = br.read(buf)) != -1 ) { } }
4. BufferedWriter(缓冲字符输出流) 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.io.BufferedWriter;import java.io.FileWriter;import java.io.IOException;import java.nio.charset.StandardCharsets;try (BufferedWriter bw = new BufferedWriter ( new FileWriter ("D:/data/out.txt" , StandardCharsets.UTF_8))) { bw.write("第一行" ); bw.newLine(); bw.write("第二行" ); bw.append("可链式追加" ); bw.flush(); }
5. 缓冲流版文件拷贝(字节流,推荐写法) 与裸流拷贝相比,套上缓冲流后 read/write 的底层调用次数大幅减少,且写端自动攒批落盘:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;try (BufferedInputStream bis = new BufferedInputStream ( new FileInputStream ("D:/data/src.bin" )); BufferedOutputStream bos = new BufferedOutputStream ( new FileOutputStream ("D:/data/dest.bin" ))) { byte [] buf = new byte [8192 ]; int len; while ((len = bis.read(buf)) != -1 ) { bos.write(buf, 0 , len); } }
要点 :
缓冲流是装饰器 ,包装基础流使用,本身不直接连文件(new BufferedReader(path) 这种写法不存在)
BufferedOutputStream / BufferedWriter **写完必须 flush()**(或依赖 close 自动 flush),否则缓冲区数据不落盘
BufferedReader.readLine() / BufferedWriter.newLine() 是按行处理文本的黄金组合
mark() / reset() 只有缓冲流(及少数流)支持,裸 FileInputStream 调用会抛 IOException
只需关闭最外层的缓冲流 即可,其 close() 会级联关闭内部被包装的基础流
字节缓冲流用于二进制文件;字符缓冲流用于文本文件,别混用
核心思路:用输入流 read(buf) 读出一块字节,立刻用输出流 write(buf, 0, len) 写出去,循环到 read 返回 -1(EOF)为止。读写在同一循环内完成,边读边写,不占额外全量内存,适合任意大小文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;try (FileInputStream fis = new FileInputStream ("D:/data/src.bin" ); FileOutputStream fos = new FileOutputStream ("D:/data/dest.bin" )) { byte [] buf = new byte [8192 ]; int len; while ((len = fis.read(buf)) != -1 ) { fos.write(buf, 0 , len); } } try (FileInputStream fis = new FileInputStream ("D:/data/src.bin" ); FileOutputStream fos = new FileOutputStream ("D:/data/dest.bin" )) { byte [] buf = new byte [8192 ]; int len; long total = 0 ; while ((len = fis.read(buf)) != -1 ) { fos.write(buf, 0 , len); total += len; } } try (FileInputStream fis = new FileInputStream ("D:/data/src.bin" ); FileOutputStream fos = new FileOutputStream ("D:/data/dest.bin" )) { fis.skip(16 ); byte [] buf = new byte [8192 ]; int len; while ((len = fis.read(buf)) != -1 ) { fos.write(buf, 0 , len); } } FileOutputStream fos = null ;try (FileInputStream fis = new FileInputStream ("D:/data/src.bin" )) { fos = new FileOutputStream ("D:/data/dest.bin" ); byte [] buf = new byte [8192 ]; int len; while ((len = fis.read(buf)) != -1 ) { fos.write(buf, 0 , len); } } catch (IOException e) { if (fos != null ) { try { fos.close(); } catch (IOException ignored) {} } new java .io.File("D:/data/dest.bin" ).delete(); throw e; }
要点 :
边读边写 :read(buf) 与 write(buf, 0, len) 必须在同一循环内,不要先 readAllBytes() 再写——否则大文件会撑爆内存
缓冲大小常用 8KB~32KB;太小则 IO 次数多,太大则浪费内存,按实际调优
write(buf, 0, len) 的 len 必须是 read 的返回值,不能直接写 buf.length,否则最后一块会多写无效字节
两个流都放进 try-with-resources,无论哪个抛异常都会自动关闭,避免句柄泄漏
更现代的替代:Files.copy(Path, Path, StandardCopyOption.REPLACE_EXISTING)(NIO.2,一行搞定)
对象流负责序列化 (对象 → 字节)与反序列化 (字节 → 对象),可把 Java 对象直接存进文件或通过网络传输。被读写的类必须 **implements Serializable**,否则抛 NotSerializableException。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import java.io.*;import java.util.ArrayList;import java.util.List;class User implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; private transient String password; public User (String name, int age, String password) { this .name = name; this .age = age; this .password = password; } } try (ObjectOutputStream oos = new ObjectOutputStream ( new BufferedOutputStream (new FileOutputStream ("D:/data/user.dat" )))) { User u = new User ("张三" , 18 , "secret" ); oos.writeObject(u); oos.flush(); } try (ObjectInputStream ois = new ObjectInputStream ( new BufferedInputStream (new FileInputStream ("D:/data/user.dat" )))) { User u = (User) ois.readObject(); System.out.println(u.getName() + " / " + u.getAge()); } List<User> users = new ArrayList <>(); users.add(new User ("A" , 1 , "p1" )); users.add(new User ("B" , 2 , "p2" )); try (ObjectOutputStream oos = new ObjectOutputStream ( new FileOutputStream ("D:/data/users.dat" ))) { oos.writeObject(users); } try (ObjectInputStream ois = new ObjectInputStream ( new FileInputStream ("D:/data/users.dat" ))) { @SuppressWarnings("unchecked") List<User> back = (List<User>) ois.readObject(); }
要点 :
被序列化类必须 implements Serializable(空接口/标记接口,仅起标识作用)
**显式声明 serialVersionUID**:类改了字段后仍能用旧 serialVersionUID 兼容反序列化,否则易 InvalidClassException
transient 修饰的字段不参与序列化 (如密码、缓存),反序列化后为默认值(引用类型 null)
static 字段也不序列化 :静态变量属于类,不属于对象实例
readObject() 返回 Object 必须强转,且类必须在 classpath 中(否则 ClassNotFoundException)
跨语言/跨版本场景优先用 JSON(如 Jackson),ObjectOutputStream 是 Java 私有格式,仅适合 Java 内部持久化
**读取多个对象不要用 while(true) + 捕获 EOFException**,用集合一次性读写更可靠
转换流是字节流 ↔ 字符流之间唯一的桥梁 :InputStreamReader 把字节输入流转成字符输入流(byte → char),OutputStreamWriter 把字符输出流转成字节输出流(char → byte),两者都能显式指定编码 ,是读写非 UTF-8 文本、避免乱码的关键。下面给出 JDK 8(手写转换流)与 JDK 11(借助 Files 工具类,内部已封装转换流)两种写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import java.io.*;import java.nio.charset.StandardCharsets;import java.nio.file.*;try (BufferedReader br = new BufferedReader ( new InputStreamReader (new FileInputStream ("D:/data/in.txt" ), StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) { } } try (BufferedWriter bw = new BufferedWriter ( new OutputStreamWriter (new FileOutputStream ("D:/data/out.txt" ), StandardCharsets.UTF_8))) { bw.write("你好" ); bw.newLine(); } try (BufferedReader br = new BufferedReader ( new InputStreamReader (new FileInputStream ("D:/data/gbk.txt" ), "GBK" ))) { } try (BufferedReader br = Files.newBufferedReader( Paths.get("D:/data/in.txt" ), StandardCharsets.UTF_8)) { String line; while ((line = br.readLine()) != null ) { } } try (BufferedWriter bw = Files.newBufferedWriter( Paths.get("D:/data/out.txt" ), StandardCharsets.UTF_8)) { bw.write("你好" ); } String content = Files.readString(Paths.get("D:/data/in.txt" ), StandardCharsets.UTF_8); Files.writeString(Paths.get("D:/data/out.txt" ), content, StandardCharsets.UTF_8);
要点 :
转换流只能包装字节流 (FileInputStream / FileOutputStream),不能包字符流;它正是字节与字符世界的唯一入口
编码在转换流处指定(new InputStreamReader(in, StandardCharsets.UTF_8)),务必显式写,别依赖平台默认编码
FileReader / FileWriter 是 InputStreamReader/OutputStreamWriter 的预制便捷类,但不能指定编码 ;需指定编码时必须手写转换流
JDK 8 :手写 InputStreamReader/OutputStreamWriter + 缓冲流;JDK 11 :优先用 Files.newBufferedReader/Writer 或 Files.readString/writeString,更简洁且内部已用转换流
读写文本几乎都该经转换流(或 Files 工具类),直接拿字节流硬转字符串容易乱码
打印流(PrintStream / PrintWriter) 打印流是输出流的包装器 ,提供 print / println / printf 等便捷方法,写什么类型都自动转成字符串,不用手动 String.valueOf 或转字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 try (PrintWriter pw = new PrintWriter ( new BufferedWriter (new FileWriter ("D:/data/log.txt" )), true )) { pw.println("普通字符串" ); pw.println(123 ); pw.println(3.14 ); pw.println(true ); pw.printf("姓名:%s,年龄:%d%n" , "张三" , 25 ); } try (PrintWriter pw = new PrintWriter ( new BufferedWriter (new OutputStreamWriter ( new FileOutputStream ("D:/data/utf8.txt" ), StandardCharsets.UTF_8)))) { pw.println("指定 UTF-8 编码写入" ); } PrintStream out = System.out;out.println("等同于 System.out.println" ); try (PrintStream ps = new PrintStream ("D:/data/stdout.txt" , "UTF-8" )) { System.setOut(ps); System.out.println("这行会写进文件而不是控制台" ); } try (PrintWriter pw = new PrintWriter ("D:/data/a.txt" )) { pw.println("hello" ); if (pw.checkError()) { throw new IOException ("写出错" ); } }
要点 :
PrintWriter 面向字符 (推荐文本),PrintStream 面向字节 (System.out 就是它)
打印流**不抛 IOException**,内部吞掉异常并置错误标志,需主动 checkError() 检查
开启自动刷新只在 println / printf / format 时触发,print 不会
printf 用 %n 而不是 \n,保证跨平台换行正确
指定编码时 PrintWriter 要套 OutputStreamWriter(它没有 Charset 构造重载);PrintStream 可以直接传编码字符串
压缩流(ZipOutputStream) 压缩流是输出流的包装器 ,写进去的数据会被自动压缩。ZipOutputStream 把多个文件/文件夹打包成 .zip,靠 ZipEntry 区分每个条目。核心三步:**putNextEntry → write → closeEntry**。由简到繁,先压单个文件,再压整个文件夹。
一、压缩单个文件 一个 ZipEntry 就是包里唯一文件,最简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static void zipOneFile (String srcFile, String zipPath) throws IOException { try (ZipOutputStream zos = new ZipOutputStream ( new BufferedOutputStream (Files.newOutputStream(Paths.get(zipPath))), StandardCharsets.UTF_8)) { Path file = Paths.get(srcFile); ZipEntry entry = new ZipEntry (file.getFileName().toString()); zos.putNextEntry(entry); Files.copy(file, zos); zos.closeEntry(); zos.close() } } package com.fanxing;import java.io.*;import java.nio.file.Path;import java.util.zip.ZipEntry;import java.util.zip.ZipOutputStream;public class Main12 { public static void main (String[] args) throws IOException { File root = new File ("D:\\tmp\\222\\html" ); ZipOutputStream zipOutputStream = new ZipOutputStream (new FileOutputStream (new File (root.getParent(), "2.zip" ))); deepFile(root, zipOutputStream, root); zipOutputStream.close(); } public static void deepFile (File root, ZipOutputStream zipOutputStream, File rootDir) throws IOException { File[] files = root.listFiles(); if (files == null ){ return ; } for (File file : files) { if (file.isDirectory()){ deepFile(file, zipOutputStream, rootDir); }else { Path relativize = rootDir.toPath().relativize(file.toPath()); ZipEntry zipEntry = new ZipEntry (relativize.toString()); zipOutputStream.putNextEntry(zipEntry); int len = 0 ; byte [] bytes = new byte [1024 *8 ]; BufferedInputStream bis = new BufferedInputStream (new FileInputStream (file)); while ((len= bis.read(bytes)) != -1 ){ zipOutputStream.write(bytes, 0 , len); } zipOutputStream.closeEntry(); } } } }
常用 API
类 / 方法
说明
new ZipOutputStream(OutputStream, Charset)
JDK 8 指定编码(如 StandardCharsets.UTF_8),避免中文名跨平台乱码
ZipOutputStream.putNextEntry(ZipEntry)
开始一个新条目(★ 必须)
ZipOutputStream.closeEntry()
结束当前条目(★ 必须,漏掉包会损坏)
ZipOutputStream.setLevel(int)
压缩级别 0~9,9 最小最慢;需在 putNextEntry 前调用才对当前条目生效
new ZipEntry("dir/a.txt")
条目名即包内相对路径;目录条目以 / 结尾
ZipEntry.setTime(long)
保留文件修改时间(可选)
要点
核心三步 putNextEntry → write → closeEntry,**closeEntry 不能漏**,否则包损坏
遍历文件夹用 root.relativize(path) 生成条目名,解压才能还原目录结构
压缩流**必须包 BufferedOutputStream**,否则每个字节都触发一次压缩器,性能极差
Zip 中文名乱码 :ZipOutputStream(OutputStream) 用平台默认编码,跨平台易乱码;JDK 8 用 new ZipOutputStream(os, StandardCharsets.UTF_8)
文本/日志压缩收益大(可到 1/5),图片/视频/已压缩文件再压几乎无收益
Zip Slip 路径穿越 (解压时校验条目名不能含 ..)见下方解压流
解压流是输入流的包装器 ,读取时被自动解压,用法与普通输入流完全一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class Main13 { private static final String path = "D:\\tmp\\222\\test2" ; public static void main (String[] args) throws IOException { new File (path).mkdirs(); ZipInputStream zis = new ZipInputStream (new FileInputStream ("D:\\tmp\\222\\html.zip" )); ZipEntry zipEntry = null ; while ((zipEntry = zis.getNextEntry()) != null ){ if (zipEntry.isDirectory()){ new File (path, zipEntry.getName()).mkdirs(); continue ; }else { FileOutputStream fos = new FileOutputStream (new File (path, zipEntry.getName())); byte [] bytes = new byte [1024 *8 ]; int len = 0 ; while ((len = zis.read(bytes)) != -1 ){ fos.write(bytes, 0 ,len); } fos.close(); } zis.closeEntry(); } zis.close(); } } try (GZIPInputStream gzis = new GZIPInputStream ( new BufferedInputStream (new FileInputStream ("D:/data/big.txt.gz" )))) { Files.copy(gzis, Paths.get("D:/data/big_restore.txt" )); } public static String decompress (byte [] compressed) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream (compressed); try (GZIPInputStream gzis = new GZIPInputStream (bais); ByteArrayOutputStream baos = new ByteArrayOutputStream ()) { byte [] buf = new byte [8192 ]; int len; while ((len = gzis.read(buf)) != -1 ) { baos.write(buf, 0 , len); } return baos.toString(StandardCharsets.UTF_8.name()); } } try (ZipFile zipFile = new ZipFile ("D:/data/archive.zip" , StandardCharsets.UTF_8)) { ZipEntry entry = zipFile.getEntry("a.txt" ); if (entry != null ) { try (InputStream is = zipFile.getInputStream(entry)) { String content = new String (is.readAllBytes(), StandardCharsets.UTF_8); } } }
要点 :
ZipInputStream 靠 getNextEntry() 驱动:返回 null 表示读完了;每读完一个条目必须 closeEntry()
ZipInputStream 无法预知条目数量 (只能顺序扫),若要按名随机取或要 size(),用 ZipFile
必须做 Zip Slip 校验 :解压前用 getCanonicalPath() 确认目标路径在预期目录内,否则恶意压缩包可写入 ../../ 覆盖系统文件
条目名编码要和压缩时一致,new ZipInputStream(in, Charset) 指定;ZipFile 同理
GZIPInputStream 没有条目概念,整个流就是一个文件的压缩数据,直接读即可
解压大文件时限制条目大小 (防止 Zip Bomb 压缩包炸弹耗尽内存/磁盘),可检查 entry.getSize() 上限
空目录条目 entry.isDirectory() 为 true,要 mkdirs() 而不是当文件写
请求三方接口(HTTP 客户端) 调用外部 HTTP 接口是开发中最常见的需求。下面按”JDK 原生 → 第三方主流”列出常用写法。
一、JDK 原生 HttpURLConnection(零依赖,JDK 8 自带) JDK 自带、无需任何依赖,简单场景直接可用;注意它不支持连接池 ,高并发请换下方第三方客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import java.io.BufferedReader;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.HttpURLConnection;import java.net.URL;import java.nio.charset.StandardCharsets;URL url = new URL ("https://api.example.com/user/1" );HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("GET" ); conn.setConnectTimeout(5000 ); conn.setReadTimeout(10000 ); conn.setRequestProperty("Accept" , "application/json" ); int code = conn.getResponseCode(); StringBuilder sb = new StringBuilder ();try (BufferedReader br = new BufferedReader ( new InputStreamReader (conn.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) sb.append(line); } System.out.println(sb.toString()); conn.disconnect(); URL postUrl = new URL ("https://api.example.com/user" );HttpURLConnection postConn = (HttpURLConnection) postUrl.openConnection();postConn.setRequestMethod("POST" ); postConn.setConnectTimeout(5000 ); postConn.setReadTimeout(10000 ); postConn.setDoOutput(true ); postConn.setRequestProperty("Content-Type" , "application/json; charset=utf-8" ); try (OutputStream os = postConn.getOutputStream()) { os.write("{\"name\":\"tom\",\"age\":18}" .getBytes(StandardCharsets.UTF_8)); } StringBuilder body = new StringBuilder ();try (BufferedReader br = new BufferedReader ( new InputStreamReader (postConn.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null ) body.append(line); } System.out.println(body.toString()); postConn.disconnect();
二、OkHttp(最流行的第三方库) 自动连接池、GZIP、失败重试,用法直观,Android 和服务端都常用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import okhttp3.*;OkHttpClient ok = new OkHttpClient .Builder() .connectTimeout(5 , java.util.concurrent.TimeUnit.SECONDS) .build(); Request get = new Request .Builder() .url("https://api.example.com/user/1" ) .addHeader("Accept" , "application/json" ) .build(); try (Response r = ok.newCall(get).execute()) { System.out.println(r.code()); System.out.println(r.body().string()); } MediaType JSON = MediaType.parse("application/json; charset=utf-8" );Request post = new Request .Builder() .url("https://api.example.com/user" ) .post(RequestBody.create("{\"name\":\"tom\"}" , JSON)) .build(); try (Response r = ok.newCall(post).execute()) { System.out.println(r.body().string()); }
三、Apache HttpClient 5(老牌企业常用) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.apache.hc.client5.http.classic.CloseableHttpClient;import org.apache.hc.client5.http.classic.methods.*;import org.apache.hc.client5.http.impl.classic.HttpClients;import org.apache.hc.core5.http.io.entity.StringEntity;try (CloseableHttpClient http = HttpClients.createDefault()) { HttpGet get = new HttpGet ("https://api.example.com/user/1" ); http.execute(get, response -> { System.out.println(response.getCode()); return null ; }); HttpPost post = new HttpPost ("https://api.example.com/user" ); post.setEntity(new StringEntity ("{\"name\":\"tom\"}" , java.nio.charset.StandardCharsets.UTF_8)); post.setHeader("Content-Type" , "application/json" ); http.execute(post, response -> { System.out.println(response.getCode()); return null ; }); }
常用客户端对比
客户端
依赖
适用场景
JDK 原生 HttpURLConnection
JDK 自带,零依赖
JDK 8 可用,简单场景够用;无连接池,高并发换第三方客户端
OkHttp
com.squareup.okhttp3:okhttp
最流行,连接池/拦截器完善,Android 首选
Apache HttpClient 5
org.apache.httpcomponents.client5:httpclient5
老牌企业项目,功能全
Spring RestTemplate
spring-web
Spring 项目快速调用(已标注弃用,新项目改用 WebClient)
要点
生产环境必须设超时 (连接超时 + 读取超时),否则对方卡住会拖垮自身线程
POST JSON 务必带 Content-Type: application/json,否则服务端可能解析失败
响应体只能读取一次,需复用先读成 String 存起来;4xx/5xx 要从 getErrorStream() 读取错误信息(HttpURLConnection)
异常要覆盖:4xx 业务错误、5xx 服务端错误、超时 SocketTimeoutException、网络不通 IOException
高并发用带连接池的客户端(OkHttp / Apache HttpClient),别每次请求 new 一个客户端实例
反射(Reflection) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 Class<?> c1 = User.class; Class<?> c2 = userObj.getClass(); Class<?> c3 = Class.forName("com.example.User" ); User u = (User) c3.getDeclaredConstructor().newInstance();User u2 = (User) c3.getDeclaredConstructor(String.class, int .class) .setAccessible(true ).newInstance("tom" , 18 ); Method m = c1.getMethod("setName" , String.class); m.invoke(u, "jerry" ); Method pm = c1.getDeclaredMethod("privateInit" ); pm.setAccessible(true ); Object ret = pm.invoke(u);Field[] pubFields = c1.getFields(); Field[] allFields = c1.getDeclaredFields(); Field f = c1.getField("name" ); Field df = c1.getDeclaredField("name" ); df.setAccessible(true ); df.getName(); df.getType(); df.getGenericType(); df.getModifiers(); Modifier.isPrivate(df.getModifiers()); Modifier.isStatic(df.getModifiers()); Modifier.isFinal(df.getModifiers()); df.getAnnotation(Deprecated.class); String name = (String) df.get(u); df.set(u, "jack" ); df.set(null , "x" ); int age = df.getInt(u); df.setInt(u, 18 ); for (Field fd : c1.getDeclaredFields()) { fd.setAccessible(true ); System.out.println(fd.getName() + " = " + fd.get(u)); } Field parentF = c1.getSuperclass().getDeclaredField("id" );Method sm = c1.getDeclaredMethod("of" , String.class);Object v = sm.invoke(null , "x" );c1.getDeclaredMethods(); c1.getDeclaredConstructors(); c1.getAnnotations();
(以上用到的 Field/Method/Constructor/Modifier 均在 java.lang.reflect 包下)
场景 :框架底层(Spring IOC、MyBatis 结果映射)、注解处理器、通用工具(Bean 拷贝、Excel 导入导出)、按配置动态加载实现类
注意 :
反射绕过编译期检查:方法重命名/改签名不会编译报错,只在运行时暴露,调用前最好做空判断与异常兜底
setAccessible(true) 构造方法是private时,暴力反射通过设置为true, 跳过private限制。
破坏封装且 JDK9+ 受模块系统限制,业务代码优先用正常调用
性能低于直接调用(JIT 难以内联),高频路径要缓存 Method/Field 或改用 MethodHandle
invoke 抛出的异常被包装成 InvocationTargetException,真实异常在 getTargetException() / getCause()
getDeclaredField(s) 只查本类,父类字段要沿 getSuperclass() 逐层查找;getField(s) 才含继承但仅限 public
final 字段即使 setAccessible(true) 也通常改不动(尤其 static final,编译期常量会被内联),别依赖反射改常量
字段 get() 返回的是包装类型(基本类型自动装箱),取值前注意 null 与类型转换
UUID 1 2 3 4 5 6 String uuid = UUID.randomUUID().toString(); String uuid32 = uuid.replace("-" , "" ); UUID named = UUID.nameUUIDFromBytes("mykey" .getBytes());
场景 :文件名、临时Token、幂等ID(注意:不适合做数据库主键,无序会导致索引效率差)
包装类 Integer.parseInt(string)
Boolean.parseBoolean(string)
1 2 System.out.println(new Boolean("true")); System.out.println(Boolean.parseBoolean("true"));
Properties(配置文件读取) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Properties props = new Properties (); try (java.io.InputStream is = Main.class.getClassLoader().getResourceAsStream("config.properties" )) { if (is == null ) { throw new IOException ("config.properties not found in classpath" ); } props.load(is); } Properties props = new Properties ();InputStream is = new FileInputStream ("路径/config.properties" ); 默认为当前根目录 props.load(is); is.close() String value = props.getProperty("db.url" );String withDefault = props.getProperty("timeout" , "3000" );props.setProperty("new.key" , "new.value" ); props.store(new FileOutputStream ("config.properties" ), "Updated config" );
场景 :读取自定义配置文件(Spring Boot 项目一般用 @Value 或 @ConfigurationProperties)
Scanner(输入读取) 1 2 3 4 5 6 7 8 9 10 11 12 Scanner sc = new Scanner (System.in);String line = sc.nextLine(); int num = sc.nextInt(); double d = sc.nextDouble(); boolean has = sc.hasNext(); Scanner fileSc = new Scanner (new File ("data.txt" ));while (fileSc.hasNextLine()) { System.out.println(fileSc.nextLine()); } fileSc.close();
场景 :命令行交互、ACM算法竞赛输入、简单文件解析
Runtime 与 Process(系统交互) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Runtime rt = Runtime.getRuntime();long maxMem = rt.maxMemory(); long totalMem = rt.totalMemory(); long freeMem = rt.freeMemory(); int cpus = rt.availableProcessors(); Process process = rt.exec("ping localhost" );try (BufferedReader reader = new BufferedReader ( new InputStreamReader (process.getInputStream()))) { String line; while ((line = reader.readLine()) != null ) { System.out.println(line); } } int exitCode = process.waitFor(); ProcessBuilder pb = new ProcessBuilder ("ls" , "-la" );pb.directory(new File ("/tmp" )); pb.redirectErrorStream(true ); Process p = pb.start();
场景 :监控JVM内存、执行系统命令(部署脚本/定时任务)、获取环境信息
Comparable 与 Comparator(排序接口) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class User implements Comparable <User> { private String name; private int age; @Override public int compareTo (User other) { return Integer.compare(this .age, other.age); } } Collections.sort(userList); userList.sort(Comparator.comparingInt(User::getAge)); userList.sort(Comparator.comparingInt(User::getAge).reversed()); userList.sort(Comparator.comparing(User::getDept).thenComparingInt(User::getAge)); userList.sort(Comparator.comparing(User::getName, Comparator.nullsLast(String::compareTo)));
场景 :List排序、TreeSet/TreeMap自定义排序规则、Stream sorted
Class 类与类型判断 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Class<?> clazz = String.class; Class<?> clazz2 = "hello" .getClass(); Class<?> clazz3 = Class.forName("java.lang.String" ); obj instanceof String String.class.isInstance(obj) String.class.isAssignableFrom(obj.getClass()) clazz.getName(); clazz.getSimpleName(); clazz.getSuperclass(); clazz.getInterfaces(); clazz.isInterface(); clazz.isEnum(); clazz.isArray();
Queue 与 Deque(队列/栈) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Queue<String> queue = new LinkedList <>(); queue.offer("A" ); queue.add("B" ); queue.peek(); queue.poll(); queue.remove(); Deque<Integer> stack = new ArrayDeque <>(); stack.push(1 ); stack.push(2 ); stack.peek(); stack.pop(); PriorityQueue<Integer> pq = new PriorityQueue <>(); PriorityQueue<Integer> maxHeap = new PriorityQueue <>(Comparator.reverseOrder()); pq.offer(5 ); pq.offer(1 ); pq.offer(3 ); pq.poll();
场景 :BFS广度搜索、任务调度、Top-K问题、表达式求值(栈)
BitSet(位集合) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 BitSet bs = new BitSet (64 );bs.set(0 ); bs.set(5 ); bs.set(10 , 20 ); bs.get(5 ); bs.clear(5 ); bs.cardinality(); bs.isEmpty(); BitSet bs2 = new BitSet ();bs2.set(3 ); bs2.set(5 ); bs.and(bs2); bs.or(bs2); bs.xor(bs2);
场景 :大数据去重/判存(布隆过滤器基础)、权限位标记、状态压缩
三、泛型(Generics)
💡 前端视角(泛型 = TypeScript 的 <T>) :Java 泛型几乎就是 TS 泛型。你写的 Result<T>、List<String> 在 TS 里是 Result<T>、Array<string>(Java 的 List ≈ TS 的 Array,Map ≈ Map)。
public static <T> Result<T> success(T data) ≈ TS 的 static success<T>(data: T): Result<T>。
类型擦除 这点特别像 TS:Java 的泛型只在编译期做类型检查,运行时被”擦掉”(运行时看不到 List<String> 里的 String);TS 的类型也是编译期存在、运行时完全消失——这就是”类型擦除”。所以两者都无法在运行时靠泛型做 instanceof 判断。
泛型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Result <T> { private int code; private String message; private T data; public static <T> Result<T> success (T data) { Result<T> result = new Result <>(); result.code = 200 ; result.message = "success" ; result.data = data; return result; } public static <T> Result<T> fail (String msg) { Result<T> result = new Result <>(); result.code = 500 ; result.message = msg; return result; } }
泛型方法 1 2 3 public static <T extends Comparable <T>> T max (T a, T b) { return a.compareTo(b) >= 0 ? a : b; }
通配符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void printList (List<? extends Number> list) { for (Number n : list) { System.out.println(n); } } public void addNumbers (List<? super Integer> list) { list.add(1 ); list.add(2 ); }
类型擦除 1 2 3 4 5 6 7 List<String> list = new ArrayList <>(); System.out.println(list.getClass());
四、异常处理(Exception)
💡 前端视角(try/catch 一模一样) :try-catch-finally 和 JS / TypeScript 语法几乎相同 ,你会非常熟悉:
1 try { } catch (e) { } finally { }
唯一明显区别是 受检异常(Checked Exception) :Java 要求某些异常(如 IOException、SQLException)必须在方法上 throws 声明、或用 try-catch 捕获,否则编译不通过 ;而 JS/TS 没有任何”强制你处理异常”的机制,漏了 catch 运行时才炸。前端转后端的人常因没这约束而忽略错误处理,Java 这点反而是”强制规范”。
异常体系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Throwable ├── Error(系统级错误,不应捕获) │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── ... └── Exception ├── RuntimeException(非受检异常,可选捕获) │ ├── NullPointerException │ ├── IndexOutOfBoundsException │ ├── IllegalArgumentException │ ├── ClassCastException │ └── ArithmeticException └── 受检异常(必须捕获或声明) ├── IOException ├── SQLException └── ClassNotFoundException
try-catch-finally 1 2 3 4 5 6 7 8 9 10 11 12 try { int result = 10 / 0 ; } catch (ArithmeticException | NullPointerException e) { System.out.println("算术异常:" + e.getMessage()); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("finally块执行" ); }
try-with-resources(JDK7+) 1 2 3 4 5 6 7 8 9 10 11 try (FileInputStream fis = new FileInputStream ("test.txt" ); BufferedReader br = new BufferedReader (new InputStreamReader (fis))) { String line; while ((line = br.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }
自定义异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class BusinessException extends RuntimeException { private int code; public BusinessException (int code, String message) { super (message); this .code = code; } public BusinessException (String message, Throwable cause) { super (message, cause); } public int getCode () { return code; } } public User findUser (Long id) { User user = userMapper.selectById(id); if (user == null ) { throw new BusinessException (404 , "用户不存在" ); } return user; }
全局异常处理(Spring Boot) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<?> handleBusiness(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result<?> handleValidation(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining("; " )); return Result.fail(400 , msg); } @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("系统异常" , e); return Result.fail(500 , "系统内部错误" ); } }
五、IO 与 NIO 传统 IO 分类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 字节流(处理二进制文件:图片、视频等) ├── InputStream │ ├── FileInputStream │ ├── BufferedInputStream │ └── ByteArrayInputStream └── OutputStream ├── FileOutputStream ├── BufferedOutputStream └── ByteArrayOutputStream 字符流(处理文本文件) ├── Reader │ ├── FileReader │ ├── BufferedReader │ └── InputStreamReader(桥接:字节→字符) └── Writer ├── FileWriter ├── BufferedWriter └── OutputStreamWriter(桥接:字符→字节)
文件读写 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static String readFile (String path) throws IOException { return new String (Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); } public static List<String> readLines (String path) throws IOException { return Files.readAllLines(Paths.get(path), StandardCharsets.UTF_8); } public static void writeFile (String path, String content) throws IOException { Files.write(Paths.get(path), content.getBytes(StandardCharsets.UTF_8)); } Files.write(Paths.get(path), content.getBytes(), StandardOpenOption.APPEND); try (BufferedReader br = Files.newBufferedReader(Paths.get(path))) { String line; while ((line = br.readLine()) != null ) { process(line); } }
File 与 Path/Files 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 File file = new File ("/path/to/file.txt" );file.exists(); file.isFile(); file.isDirectory(); file.length(); file.getName(); file.getParent(); file.listFiles(); Path path = Paths.get("/path/to/file.txt" );Files.exists(path); Files.size(path); Files.createDirectories(Paths.get("/path/to/dir" )); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); Files.move(source, target); Files.delete(path); Files.walk(Paths.get("/root" )) .filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".java" )) .forEach(System.out::println);
序列化
💡 前端视角(序列化 = JSON.stringify / JSON.parse) : 把”对象 ↔ 字符串”互相转换,在 JS 里就是 JSON.stringify(obj) 和 JSON.parse(str)。Java 这边只是换了个工具类名字:
Java 工具
等价前端
说明
mapper.writeValueAsString(user)(Jackson)
JSON.stringify(user)
对象 → JSON 字符串
mapper.readValue(json, User.class)(Jackson)
JSON.parse(json)
JSON 字符串 → 对象(**需指定目标类型 User.class**,JS 不用)
gson.toJson / fromJson
同上
Google 的 Gson
关键差异:JS 的 JSON.parse 不需要告诉它”解析成什么类型”(运行时 JS 自己推断);**Java 是强类型,必须显式传入 User.class 这类”目标 Class”**,反序列化才知道把字段塞进哪个类。这也是为什么 Java 里常看到 new TypeReference<List<User>>(){} 这种”为了告诉编译器泛型类型”的写法。
1 2 3 4 5 6 7 8 9 10 11 12 public class User implements Serializable { private static final long serialVersionUID = 1L ; private String name; private transient String password; } ObjectMapper mapper = new ObjectMapper ();String json = mapper.writeValueAsString(user); User user = mapper.readValue(json, User.class); List<User> users = mapper.readValue(json, new TypeReference <List<User>>() {});
💡 运用场景:什么时候需要把对象”序列化”? 只要对象要离开当前 JVM 内存 (跨进程/跨网络/落磁盘),就需要先变成字节流或字符串:
RPC 远程调用 :微服务 A 调 B,参数对象要先序列化(JSON / Protobuf / Hessian)再通过网络传输,B 再反序列化。这是微服务通信的基石。
Redis 缓存 :把 User 对象 writeValueAsString 成字符串存进 Redis,取出来再 readValue 还原。
HttpSession / 分布式会话 :用户登录信息存 Session(可能序列化到磁盘或共享存储),重启不丢。
消息队列(MQ) :发送给 Kafka/RabbitMQ 的消息体必须是可序列化的字符串/字节。
选型建议:内部服务间用 JSON(Jackson) 最通用可读;追求性能/体积用 Protobuf/Kryo ;Java 原生 Serializable 基本不用于网络传输 (版本兼容脆弱、体积大)。transient 字段(如密码)不会被序列化,保护敏感数据。
六、多线程与并发 创建线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 class MyThread extends Thread { @Override public void run () { System.out.println(Thread.currentThread().getName() + " running" ); } } new MyThread ().start();class DownloadTask implements Runnable { private final String url; public DownloadTask (String url) { this .url = url; } @Override public void run () { System.out.println(Thread.currentThread().getName() + " 开始下载:" + url); try { Thread.sleep(1000 ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return ; } System.out.println(Thread.currentThread().getName() + " 下载完成:" + url); } } Runnable shared = new DownloadTask ("shared.zip" );new Thread (shared, "worker-2" ).start();new Thread (shared, "worker-3" ).start();class SumTask implements Callable <Integer> { private final int n; public SumTask (int n) { this .n = n; } @Override public Integer call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= n; i++) { sum += i; Thread.sleep(10 ); } return sum; } } FutureTask<Integer> task = new FutureTask <>(new SumTask (10 )); new Thread (task, "calc-thread" ).start();Integer result = task.get(); System.out.println("结果:" + result); task.isDone(); task.isCancelled(); task.cancel(true ); try { task.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); } catch (CancellationException e) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } Future<Integer> future = pool.submit(new SumTask (10 )); Integer value = future.get();
线程的状态(Thread.State,6 种)
状态
含义
典型触发场景
NEW
新建,还没启动
new Thread(...) 之后、调用 start() 之前
RUNNABLE
可运行
start() 之后;包含”就绪(等 CPU)”和”运行中”两种,JVM 不区分
BLOCKED
阻塞(等锁)
竞争 synchronized 监视器锁失败
WAITING
无限期等待
Object.wait()、join()、LockSupport.park()(需被别人唤醒)
TIMED_WAITING
限期等待
sleep(ms)、wait(ms)、join(ms)、parkNanos(超时自动醒)
TERMINATED
终止
run() 正常结束或抛出未捕获异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Thread t = new Thread (() -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); Thread.sleep(100 ); System.out.println(t.getState()); t.join(); System.out.println(t.getState());
状态流转
1 2 3 4 5 NEW ──start()──▶ RUNNABLE ──run() 结束 / 未捕获异常──▶ TERMINATED │ ├─抢 synchronized 锁失败──▶ BLOCKED ──拿到锁──▶ RUNNABLE ├─wait() / join() / park()──▶ WAITING ──notify()/notifyAll()/unpark()/目标线程结束──▶ RUNNABLE └─sleep(ms) / wait(ms) / join(ms) / parkNanos──▶ TIMED_WAITING ──超时/被唤醒──▶ RUNNABLE
BLOCKED 只针对 synchronized;用 ReentrantLock 抢锁失败的线程状态是 WAITING/TIMED_WAITING(底层走 LockSupport.park()),排查锁竞争时不要只看 BLOCKED
WAITING 与 TIMED_WAITING 的区别只有一个:是否有超时时间;WAITING 只能被别人唤醒,唤醒不了就一直挂着
线程进入 TERMINATED 后**不能再 start()**(重复调用抛 IllegalThreadStateException),要复用请用线程池
getState() 只是采样瞬间 的状态,排障时通常配合 jstack 看线程栈,能直接看到卡在哪一行、等哪把锁
setDaemon (boolean on) 设置为备胎,当主线程结束时,不管当前有没有执行完都结束
Thread.yield() 多个线程同时执行时尽可能均匀
.join() 多线程本身类似于微任务,添加 join后,可以让当前线程执行完成后再往下执行(FutrueTask包装的除外)
要点 :
分工 :Runnable = 任务(做什么),Thread = 执行者(谁来做)。实现接口比 extends Thread 更灵活——Java 是单继承,继承 Thread 后就无法再继承别的父类
必须调 start()**:start() 才由 JVM 创建新线程并回调 run();直接写 t.run() 只是在当前线程里当普通方法执行, 不会并发**
函数式接口 :Runnable 只有 run() 一个抽象方法,所以支持 Lambda / 方法引用简写;需要复用、带参数、带状态时仍应显式定义实现类
不能抛受检异常 :run() 的签名没有 throws,异常只能在方法内 try-catch 处理(InterruptedException 要恢复中断标记)
**Callable.call() vs Runnable.run()**:前者有返回值、可声明 throws;后者都没有——需要”结果”或”异常可感知”时选 Callable(方式3)
FutureTask 是二合一 :同时实现 Runnable + Future,所以既可以直接扔给 Thread,也能当 Future 取结果;线程池 submit(Callable) 内部就是它
get() 阻塞 :会一直等到任务结束,**强烈建议用带超时的 get(timeout, unit)**,避免线程被无限挂住
异常不在子线程抛出 :call() 里的异常被 FutureTask 捕获,只在 get() 时包成 ExecutionException(用 getCause() 取原始异常);任务被 cancel 后 get() 抛 CancellationException
选型 :需要返回值 / 抛异常 / 取消任务用 Callable + FutureTask(方式3);实际项目中任务都提交给线程池 执行,而不是自己 new Thread();若要组合多个异步结果 / 回调,用 JDK8+ 的 CompletableFuture
线程池(重点) CPU密集型线程 = N线程+1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ThreadPoolExecutor pool = new ThreadPoolExecutor ( 5 , 10 , 60 , TimeUnit.SECONDS, new LinkedBlockingQueue <>(100 ), new ThreadFactory () { private final AtomicInteger counter = new AtomicInteger (1 ); @Override public Thread newThread (Runnable r) { return new Thread (r, "biz-pool-" + counter.getAndIncrement()); } }, new ThreadPoolExecutor .CallerRunsPolicy() ); pool.execute(() -> doSomething()); Future<String> future = pool.submit(() -> "result" ); pool.shutdown(); pool.shutdownNow();
Executors 工厂方法(了解原理,生产慎用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.util.concurrent.*;ExecutorService fixedPool = Executors.newFixedThreadPool(5 );List<Future<Integer>> futures = new ArrayList <>(); for (int i = 1 ; i <= 10 ; i++) { final int taskId = i; futures.add(fixedPool.submit(() -> { TimeUnit.MILLISECONDS.sleep(100 ); return taskId * 10 ; })); } int sum = 0 ;for (Future<Integer> f : futures) { sum += f.get(3 , TimeUnit.SECONDS); } System.out.println("sum = " + sum); fixedPool.shutdown(); if (!fixedPool.awaitTermination(10 , TimeUnit.SECONDS)) { fixedPool.shutdownNow(); } ExecutorService cachedPool = Executors.newCachedThreadPool();for (String url : urls) { cachedPool.execute(() -> fetch(url)); } Future<String> f = cachedPool.submit(() -> "ok" ); System.out.println(f.get()); cachedPool.shutdown();
案例要点 :
execute(Runnable) 无返回值;submit(Callable/Runnable) 返回 Future,可用于取结果、取消、感知异常
提交后要 shutdown(),否则线程一直存活、JVM 不退出;等结果推荐配合 awaitTermination
任务内的异常不会打印到主线程,必须 try-catch 或在 Future.get() 处捕获 ExecutionException
两种线程池的底层参数对比:
线程池
核心线程数
最大线程数
存活时间
任务队列
隐患
newFixedThreadPool(n)
n
n(固定)
0(不回收)
LinkedBlockingQueue(无界 ,默认 Integer.MAX_VALUE)
任务堆积 →OOM
newCachedThreadPool()
0
Integer.MAX_VALUE(无上限 )
60s
SynchronousQueue(不存储,直接移交)
高并发下疯狂创建线程 →OOM / CPU 打满
为什么不推荐(阿里规约强制禁止 Executors)
newFixedThreadPool 用无界队列 ,任务无限堆积,最终 OOM,且队列太大时 maxPoolSize 和拒绝策略形同虚设(根本不会触发)
newCachedThreadPool 最大线程数是 Integer.MAX_VALUE,相当于无限创建线程 ,每个线程默认占 1MB 栈内存,高并发下直接 OOM
二者都隐藏了关键参数 (队列大小、拒绝策略、线程命名),出问题无法定位、无法防御
正确做法:手动 new ThreadPoolExecutor(...),显式指定有界队列 + 自定义 ThreadFactory(命名)+ 合理的拒绝策略
等价手写替换
1 2 3 4 5 6 7 8 9 10 11 12 new ThreadPoolExecutor (5 , 5 , 0L , TimeUnit.MILLISECONDS, new LinkedBlockingQueue <>(1000 ), namedFactory, new ThreadPoolExecutor .AbortPolicy()); new ThreadPoolExecutor (0 , 50 , 60L , TimeUnit.SECONDS, new SynchronousQueue <>(), namedFactory, new ThreadPoolExecutor .AbortPolicy());
使用场景
场景
推荐
生产环境
手动 ThreadPoolExecutor,有界队列 + 命名 + 拒绝策略
本地测试 / 临时 Demo
Executors.newFixedThreadPool(n)(任务量可控,风险低)
大量短期异步小任务、并发量极低
newCachedThreadPool()(用完自动回收,但仍不建议生产用)
拒绝策略:
策略
行为
AbortPolicy
抛 RejectedExecutionException(默认)
CallerRunsPolicy
由调用线程执行该任务
DiscardPolicy
静默丢弃
DiscardOldestPolicy
丢弃队列最老任务,重试提交
synchronized 与 Lock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 public class Order implements Runnable { private static int sum = 0 ; public void pay () { while (true ){ synchronized (Order.class){ if (sum >= 100 ){ break ; }else { sum++; System.out.println(Thread.currentThread().getName()+"售出1张票,剩余" + (100 - sum) +"张票" ); } } } } @Override public void run () { pay(); } } package com.test2;public class Order implements Runnable { private static int sum = 0 ; public void pay () throws InterruptedException { while (true ){ if (setOrderCount()){ break ; } } } public synchronized Boolean setOrderCount () throws InterruptedException { if (sum >= 100 ){ return true ; }else { sum++; Thread.sleep(100 ); System.out.println(Thread.currentThread().getName()+"售出1张票," +"已出售" + sum + "剩余" + (100 - sum) +"张票" ); return false ; } } @Override public void run () { try { pay(); } catch (InterruptedException e) { throw new RuntimeException (e); } } } package com.test2;public class UserView { public static void main (String[] args) { Order order = new Order (); new Thread (order,"窗口1" ).start(); new Thread (order,"窗口2" ).start(); new Thread (order,"窗口3" ).start(); } } package com.test2;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Order implements Runnable { private static int sum = 0 ; private static final Lock lock = new ReentrantLock (); public void pay () { while (true ) { lock.lock(); try { if (sum >= 100 ) { break ; } sum++; System.out.println(Thread.currentThread().getName() + "售出1张票,已出售" + sum + ",剩余" + (100 - sum) + "张票" ); } finally { lock.unlock(); } } } @Override public void run () { pay(); } } public void payTryLock () { while (true ) { if (lock.tryLock()) { try { if (sum >= 100 ) break ; sum++; System.out.println(Thread.currentThread().getName() + "售出1张票" ); } finally { lock.unlock(); } } else { System.out.println(Thread.currentThread().getName() + "未拿到锁,稍后重试" ); } } } if (lock.tryLock(1 , TimeUnit.SECONDS)) { try { } finally { lock.unlock(); } } private static final Lock condLock = new ReentrantLock ();private static final Condition notFull = condLock.newCondition();public void produce () throws InterruptedException { condLock.lock(); try { while (sum >= 100 ) { notFull.await(); } sum++; notFull.signalAll(); } finally { condLock.unlock(); } }
Lock vs synchronized 对比
维度
synchronized
Lock(ReentrantLock)
加锁/释放
JVM 自动(代码块结束或异常自动释放)
必须手动 ,unlock 放 finally
可中断
不可中断
lockInterruptibly() 可响应中断
超时
不支持
tryLock(timeout) 支持
非阻塞尝试
不支持
tryLock() 支持
公平锁
非公平
可构造公平锁 new ReentrantLock(true)
条件队列
单一 wait set
多个 Condition,精准唤醒
性能
JDK6 后已优化,低竞争下更优
高竞争下更稳定
注意 :ReentrantLock 是可重入锁,同一线程可重复获取,但获取几次就要释放几次。
volatile 与原子类 1 2 3 4 5 6 7 8 9 private volatile boolean running = true ;AtomicInteger count = new AtomicInteger (0 );count.incrementAndGet(); count.getAndIncrement(); count.addAndGet(5 ); count.compareAndSet(5 , 10 );
CompletableFuture(JDK8+ 异步编程) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return queryFromDB(); }); CompletableFuture<String> result = CompletableFuture .supplyAsync(() -> getUserId()) .thenApply(id -> getUserById(id)) .thenApply(user -> user.getName()) .exceptionally(e -> "默认值" ); CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello" ); CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World" ); CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + " " + b); CompletableFuture.allOf(f1, f2, f3).join(); CompletableFuture.anyOf(f1, f2, f3).thenAccept(System.out::println);
常见案例:并行调用多个服务
1 2 3 4 5 6 7 8 9 10 11 12 13 public OrderDetailVO getOrderDetail (Long orderId) { CompletableFuture<Order> orderFuture = CompletableFuture .supplyAsync(() -> orderService.getById(orderId)); CompletableFuture<List<OrderItem>> itemsFuture = CompletableFuture .supplyAsync(() -> orderItemService.listByOrderId(orderId)); CompletableFuture<User> userFuture = orderFuture .thenCompose(order -> CompletableFuture .supplyAsync(() -> userService.getById(order.getUserId()))); CompletableFuture.allOf(orderFuture, itemsFuture, userFuture).join(); return buildVO(orderFuture.join(), itemsFuture.join(), userFuture.join()); }
并发工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CountDownLatch latch = new CountDownLatch (3 );for (int i = 0 ; i < 3 ; i++) { pool.execute(() -> { try { doWork(); } finally { latch.countDown(); } }); } latch.await(); CyclicBarrier barrier = new CyclicBarrier (3 , () -> System.out.println("全部到齐" ));for (int i = 0 ; i < 3 ; i++) { pool.execute(() -> { prepare(); barrier.await(); execute(); }); } Semaphore semaphore = new Semaphore (5 ); semaphore.acquire(); try { doWork(); } finally { semaphore.release(); }
线程安全集合 1 2 3 4 5 6 7 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap <>(); CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList <>(); BlockingQueue<String> queue = new LinkedBlockingQueue <>(100 ); queue.put("task" ); String task = queue.take();
二十二、面试高频知识点速记 HashMap 原理
数组 + 链表 + 红黑树(JDK8+,链表长度>8且数组长度≥64时树化)
初始容量16,负载因子0.75,扩容为2倍
计算桶位:(n-1) & hash
线程不安全(并发 put 可能丢数据),用 ConcurrentHashMap
Spring IOC/AOP
IOC :控制反转,对象创建交给Spring容器管理,通过DI注入依赖
AOP :面向切面编程,基于动态代理(JDK代理/CGLIB),用于日志、事务、权限等横切关注点
Bean 生命周期 :实例化 → 属性赋值 → Aware接口 → BeanPostProcessor前置 → 初始化 → BeanPostProcessor后置 → 使用 → 销毁
Bean 作用域 :singleton(默认)、prototype、request、session
Spring 事务传播行为
传播行为
说明
REQUIRED(默认)
有则加入,无则新建
REQUIRES_NEW
总是新建,挂起当前事务
SUPPORTS
有则加入,无则非事务运行
NOT_SUPPORTED
非事务运行,挂起当前事务
NESTED
有则嵌套(savepoint),无则新建
TCP 三次握手 / 四次挥手 1 2 3 4 5 6 7 8 9 10 三次握手(建立连接): Client → SYN → Server Client ← SYN+ACK ← Server Client → ACK → Server 四次挥手(断开连接): Client → FIN → Server Client ← ACK ← Server Client ← FIN ← Server Client → ACK → Server(等待 2MSL 后关闭)
线程状态 1 2 3 4 5 6 NEW → (start) → RUNNABLE → (获取锁/IO完成) → RUNNING RUNNING → (yield/时间片到) → RUNNABLE RUNNING → (wait/join/park) → WAITING RUNNING → (sleep/带超时wait) → TIMED_WAITING RUNNING → (synchronized未获取锁) → BLOCKED RUNNING → (执行完/异常) → TERMINATED