java开发文档
邵预鸿 Lv5

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>>();

// 光标停在表达式上,按 Ctrl + Alt + V:
// 1. IDEA 弹出变量名建议(默认 map)
// 2. 回车确认 → 自动补出左侧声明 + 类型推断(菱形 <> 自动保留)
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; // -128(溢出)

注意事项

  • 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; // true (地址相同)
    s1 == s3; // false (地址不同)
    s1.equals(s3); // true (内容相同)
  • 重写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; // 如果是public 子类可以直接取this.name, private则找不到
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; // == public static final int a = 100

void method(); // === public abstract void method()
}

接口继承

1
2
3
4
5
6
7
8
9
10
11
12
public interface IT1{
public abstract method1();
}

// 继承了IT1
public interface IT2 extends IT1{
public abstract method2();
}

public class A implements IT2{
// 需要重写method1() method2()
}

JDK8+接口新增方法

  • 接口中支持默认方法,且能写方法体,支持多个默认方法体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public interface  Interfa{
    public default void show(){
    // 打印
    })
    public default int getAge(){
    return 22;
    }
    public abstract void method();
    }


    public class A implements Interfa{
    // 只需要重写method
    // 默认方法show此时可不重写

    // 如果要重写默认方法
    public void show(){
    // 重写
    }
    }
  • 继承了两个接口,且两个接口都 有相同 的默认方法体时,必需重写

  • 新增static静态方法

    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
    public interface Payment {
    void pay(double amount); // 简写 - 等价于 public asbstract void pay(double amount)

    // 静态方法:提供汇率转换工具
    static double convertCurrency(double amount, String from, String to) {
    // 模拟汇率转换逻辑
    if ("USD".equals(from) && "CNY".equals(to)) {
    return amount * 7.2;
    }
    return amount;
    }

    // 静态方法:验证支付信息
    static boolean validatePayment(double amount, String currency) {
    return amount > 0 && (currency != null && !currency.isEmpty());
    }
    }

    // 使用方式 - 不需要实现类
    public class Main {
    public static void main(String[] args) {
    // 直接通过接口调用静态方法
    double cny = Payment.convertCurrency(100, "USD", "CNY");
    System.out.println(cny); // 720.0

    boolean valid = Payment.validatePayment(100, "CNY");
    System.out.println(valid); // true

    // ❌ 不能通过实现类调用
    // PaymentImpl.convertCurrency(...); // 编译错误
    }
    }
  • 接口默认方法体调用函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public interface InterA{
    public default void show1(){
    log();
    }
    public default void show2(){
    log();
    }

    private void log(){
    }

    }

内部类

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);
// 方法中如果创建类,因为是局部类,所以不能使用public修饰

}
}
}

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); // 从src[1]起复制3个到dest[0] → dest=[2,3,4,0,0]

引用数据类型时:子类数组可赋值给父类数组(多态);复制的是引用(浅拷贝),改后对象属性,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);
}
}
  1. 需要在User类中实现重写Clone方法
  2. 需要在User类中实现Cloneable接口
  3. 拷贝时需要加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); // 转为string
User u2 = gson.fromJson(s, User.class); // 从string再转回json

// 原理等价于前端的: JSON.parse(JSON.stringify(u1))

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"); // 21位,long 装不下
BigInteger b = new BigInteger("111111111111111111111");

Random random = new Random();
BigInteger bi1 = new BigInteger(10, random); // 生成一个 10 位的随机整数
System.out.println(bi1); // 输出:0 ~ 2^10-1 之间的随机数

取值范围与类型转换

  • 取值范围:无固定上下限,仅受内存限制(实践可支持约 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
// valueOf:接收 long,可能复用缓存
BigInteger a = BigInteger.valueOf(16);
BigInteger b = BigInteger.valueOf(16);
System.out.println(a == b); // true(16 在缓存范围内,同一对象)
System.out.println(a.equals(b)); // true

BigInteger c = BigInteger.valueOf(17);
BigInteger d = BigInteger.valueOf(17);
System.out.println(c == d); // false(17 超出缓存范围,新建对象)

// 构造方法:每次都是新对象(即使值相同)
BigInteger e = new BigInteger("16");
BigInteger f = new BigInteger("16");
System.out.println(e == f); // false
System.out.println(e.equals(f)); // true

重要原则:无论用哪种方式创建,**比较值都必须用 .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); // 1=a>b,0=相等,-1=a<b
BigInteger pow = a.pow(3); // a 的三次方
boolean prime = a.isProbablePrime(10); // 是否大概率素数

常见业务案例:超大订单号 / 分布式ID 拼接

1
2
3
4
// 用 BigInteger 拼接「时间戳 + 自增序列」生成不溢出、不重复的订单号
BigInteger orderId = new BigInteger(System.currentTimeMillis() + "")
.multiply(BigInteger.valueOf(10000))
.add(BigInteger.valueOf(seq)); // seq 为 0~9999 的序列号

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");
// 禁止:double 构造会带入浮点误差
// new BigDecimal(0.1) → 实际是 0.1000000000000000055511151231257827021181583404541015625

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); // 保留2位,四舍五入

// 比较大小(不要用 equals,因为 equals 会比较精度)
a.compareTo(b); // 1=a>b, 0=相等, -1=a<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); // 59.97
BigDecimal discount = total.multiply(new BigDecimal("0.85"))
.setScale(2, RoundingMode.HALF_UP); // 打85折,保留两位 → 50.97

// 百分比计算:利率 3.65% 年化
BigDecimal rate = new BigDecimal("0.0365");
BigDecimal interest = new BigDecimal("10000")
.multiply(rate).setScale(2, RoundingMode.HALF_UP); // 365.00

💡 运用场景:任何”钱 / 比率”都别用 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
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");
sb.insert(5, ","); // Hello, World
sb.reverse(); // dlroW ,olleH
String result = sb.toString();

// StringJoiner
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("Java").add("Python").add("Go");
System.out.println(sj); // [Java, Python, Go]

// String.join (JDK8+)
String joined = String.join("-", "2026", "07", "09"); // "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(); // 11
s.charAt(0); // 'H'
s.substring(0, 5); // "Hello"
s.indexOf("World"); // 6
s.contains("llo"); // true
s.startsWith("He"); // true
s.endsWith("ld"); // true
s.trim(); // 去首尾空格
s.strip(); // JDK11+,去首尾空白(含全角空格)
s.replace("World", "Java"); // "Hello Java"
s.replaceAll("\\s+", "-"); // 正则替换
s.split(" "); // ["Hello", "World"]
s.toUpperCase(); // "HELLO WORLD"
s.toLowerCase(); // "hello world"
s.isEmpty(); // false
s.isBlank(); // JDK11+,空白也返回true
String.format("Hi %s, age %d", "Tom", 25); // "Hi Tom, age 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); // 排序 → [1, 3, 5, 8, 9]
Arrays.sort(arr, 0, 3); // 区间排序


int index = Arrays.binarySearch(arr, 5); // 二分查找(需先排序)
int[] copy = Arrays.copyOf(arr, 10); // 复制(不足补0)
int[] range = Arrays.copyOfRange(arr, 1, 4); // 区间复制 [1,4)
Arrays.fill(arr, 0); // 全部填充为0
boolean eq = Arrays.equals(arr1, arr2); // 比较内容是否相等
String str = Arrays.toString(arr); // "[1, 3, 5, 8, 9]"

// 对象数组排序
User[] users = ...;
Arrays.sort(users, Comparator.comparingInt(User::getAge));

// 数组转Stream
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); // 从索引0到69,填充1
Arrays.fill(arr, 70, 100, 2); // 从索引70到99,填充2

// 验证
System.out.println(Arrays.toString(arr));
// 输出:前70个1,后30个2

场景:数据预处理、排序、查找、数组↔集合转换

常见案例:批量初始化区间值

1
2
int[] idx = new int[10];
Arrays.setAll(idx, i -> i * 2); // [0,2,4,...,18]

lambda 表达式的使用

当方法参数是函数式接口(只有一个抽象方法的接口,如 Comparator、IntFunction)时,可用 lambda 简化:

1
2
3
4
5
6
7
8
// 能直接用 lambda:参数就是函数式接口
Arrays.sort(users, (a, b) -> a.getAge() - b.getAge()); // Comparator
Arrays.setAll(arr, i -> i * 2); // IntFunction

// 不能用 lambda:参数不是函数式接口
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); // 循环右移2位
int max = Collections.max(list); // 最大值
int min = Collections.min(list); // 最小值
int freq = Collections.frequency(list, 1); // 元素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();

// N个相同元素
List<String> copies = Collections.nCopies(5, "hello"); // ["hello" x 5]

场景:排序、安全包装、构造特殊集合、算法操作


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;

// 安全的 equals(避免NPE)
Objects.equals(null, "abc"); // false,不会NPE
Objects.equals("abc", "abc"); // true

// 判空
Objects.isNull(obj); // obj == null
Objects.nonNull(obj); // obj != null
Objects.requireNonNull(obj); // 为null则抛 NullPointerException
Objects.requireNonNull(obj, "参数不能为空"); // 带消息
Objects.requireNonNull(obj, () -> "延迟构建消息"); // JDK10+

// hashCode(null安全)
int hash = Objects.hashCode(obj); // null返回0
int hash2 = Objects.hash(name, age, email); // 多字段组合hash

// toString(null安全)
String s = Objects.toString(obj, "默认值"); // null时返回默认值

// 比较
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); // false(引用不同)
System.out.println(matrix1.equals(matrix2)); // false(调用了Object的equals)
System.out.println(Objects.equals(matrix1, matrix2)); // false(调用了数组的equals,依然是Object的)
System.out.println(Objects.deepEquals(matrix1, matrix2)); // true(逐个比较元素)

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);           // 10(绝对值)
Math.max(3, 7); // 7
Math.min(3, 7); // 3
Math.ceil(3.2); // 4.0(向上取整)
Math.floor(3.8); // 3.0(向下取整)
Math.round(3.5f); // 4(四舍五入)
Math.pow(2, 10); // 1024.0(幂运算)
Math.sqrt(16); // 4.0(平方根)
Math.log(Math.E); // 1.0(自然对数)
Math.random(); // [0.0, 1.0) 随机数

// 生成 [min, max] 范围随机整数(含两端)
int random = (int)(Math.random() * (max - min + 1)) + min;

// JDK15+ 安全的绝对值(超出范围抛异常而非返回负数)
Math.absExact(Integer.MIN_VALUE); // 抛 ArithmeticException

场景:数值计算、随机数生成、金融舍入、几何/三角函数

常见案例:保留 N 位小数(配合 BigDecimal 更安全)

1
2
// 仅展示用:round 不能直接指定小数位,需先放大再缩回
double v = Math.round(3.14159 * 100) / 100.0; // 3.14(注意浮点误差)

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 random = new Random();
int num = random.nextInt(100); // [0, 100)
double d = random.nextDouble(); // [0.0, 1.0)
boolean b = random.nextBoolean();

// ThreadLocalRandom(多线程推荐,无锁)
int num2 = ThreadLocalRandom.current().nextInt(1, 101); // [1, 100]
long l = ThreadLocalRandom.current().nextLong(1000);

// SecureRandom(密码学安全,用于生成Token/验证码)
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); // 100000~999999

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); // ["a", "", "b", "c"] -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)); // true
System.out.println("ABC".matches(regex)); // true
System.out.println("AbC".matches(regex)); // true

String regex2 = "a((?i)b)c"; // 只忽略b的大小写
分组

分组组号匹配

\\1 意思是(.)相同内容,如最终能匹配的是 a

1
String reg = "(.).+\\1"

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,";  // 注意最后有一个逗号

// 1. limit = -1:保留所有空串(包括尾部的)
String[] parts1 = str.split(",", -1);
System.out.println(Arrays.toString(parts1));
// 输出: [a, "", b, c, ""] ← 尾部空串被保留

// 2. limit = 0(默认):丢弃尾部空串
String[] parts2 = str.split(",", 0);
System.out.println(Arrays.toString(parts2));
// 输出: [a, "", b, c] ← 尾部空串被丢弃

// 3. 不传 limit(等同于 0)
String[] parts3 = str.split(",");
System.out.println(Arrays.toString(parts3));
// 输出: [a, "", b, c] ← 同上
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
// 捕获组:用 () 包裹,通过 group(n) 获取
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("2026-07-21");
if (m.matches()) {
m.group(0); // "2026-07-21"(整体匹配)
m.group(1); // "2026"(第1组)
m.group(2); // "07"(第2组)
m.group(3); // "21"(第3组)
}

// 命名捕获组:(?<name>regex)
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"); // "2026"
m.group("month"); // "07"
m.group("day"); // "21"
}

// 非捕获组:(?:regex) —— 分组但不捕获,不占用 group 编号
Pattern p = Pattern.compile("(?:https?://)([\\w.-]+)");
Matcher m = p.matcher("https://www.example.com");
if (m.find()) {
m.group(1); // "www.example.com"(第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()); // "199.50"、"15.99"
}

// 示例:密码校验(至少8位,包含字母和数字)
String regex = "^(?=.*[a-zA-Z])(?=.*\\d).{8,}$";
"abc12345".matches(regex); // true
"abcdefgh".matches(regex); // false(缺数字)
7. 反向引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \n 引用第 n 个捕获组匹配到的内容
// 匹配连续重复单词
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()); // "the the"、"fox fox"
}

// 匹配成对的 HTML 标签
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()); // "<b>bold</b>"、"<i>italic</i>"
}
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");

// COMMENTS 模式(复杂正则可读性增强)
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
// ========== 1. 校验 ==========
boolean isValid = Pattern.matches("\\d+", "12345"); // true
// 等价于
boolean isValid = "12345".matches("\\d+");

// ========== 2. 查找所有匹配 ==========
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("价格100元,数量5个,总计500元");
List<String> results = new ArrayList<>();
while (m.find()) {
results.add(m.group()); // ["100", "5", "500"]
}

// ========== 3. 替换 ==========
// 全部替换
String result = "hello 123 world 456".replaceAll("\\d+", "*");
// → "hello * world *"

// 使用捕获组引用替换($1 引用第一个组)
String result = "2026-07-21".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$1年$2月$3日");
// → "2026年07月21日"

// appendReplacement 逐个控制替换
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);
// → "a[1位数]b[2位数]c[3位数]"

// ========== 4. 分割 ==========
String[] parts = "one,two,,four".split(","); // ["one","two","","four"](尾部空串被删)
String[] parts = "one,two,,four".split(",", -1); // ["one","two","","four"](保留所有空串)

// ========== 5. 转义用户输入 ==========
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
// ✅ 预编译(推荐:Pattern 对象设为 static final 复用)
private static final Pattern PATTERN = Pattern.compile("\\d+");

// ❌ 避免在循环中重复编译
for (String s : list) {
s.matches("\\d+"); // 每次调用都会重新编译 Pattern
}

// ✅ 正确做法
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"); // 对 "aaaaaaaaac" 会极慢
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 之间的数

SimpleDateFormat

常见: yyyy-MM-dd HH:mm:ss

字母 日期或时间元素 表示 示例
G Era 标志符 Text AD
y 年 Year 1996; 96
M 年中的月份 Month July; Jul; 07
w 年中的周数 Number 27
W 月份中的周数 Number 2
D 年中的天数 Number 189
d 月份中的天数 Number 10
F 月份中的星期 Number 2
E 星期中的天数 Text Tuesday; Tue
a Am/pm 标记 Text PM
H 一天中的小时数(0-23) Number 0
k 一天中的小时数(1-24) Number 24
K am/pm 中的小时数(0-11) Number 0
h am/pm 中的小时数(1-12) Number 12
m 小时中的分钟数 Number 30
s 分钟中的秒数 Number 55
S 毫秒数 Number 978
z 时区 General time zone Pacific Standard Time; PST; GMT-08:00
Z 时区 RFC 822 time zone -0800

格式化和解析日期

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;

// ============ 1. 格式化:Date → String ============
Date now = new Date();

SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s1 = sdf1.format(now); // "2026-07-30 14:30:00"

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 E a hh:mm");
String s2 = sdf2.format(now); // "2026年07月30日 星期三 下午 02:30"

SimpleDateFormat sdf3 = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String s3 = sdf3.format(now); // "20260730143000123"(常用于文件名、流水号)

// ============ 2. 解析:String → Date ============
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
try {
Date date = parser.parse("2026-12-25"); // 圣诞节
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}

// 注意:解析时格式必须与字符串匹配,否则抛 ParseException
// 例如字符串 "2026/07/30" 必须用 new SimpleDateFormat("yyyy/MM/dd")

// ============ 3. 时间戳 ↔ 格式化字符串 ============
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();

// ============ 4. 线程安全问题(重要!) ============
// ❌ 错误:SimpleDateFormat 是非线程安全的,不要做 static 共享
// static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd"); // 多线程会串数据

// ✅ 方案一:每次 new(简单但频繁创建有开销)
String result = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

// ✅ 方案二:ThreadLocal 包装(推荐旧项目用)
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());

// ✅ 方案三(推荐新项目):直接用 JDK8 的 DateTimeFormatter(不可变、线程安全)
// DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

常见格式模板速查:

模板 输出示例 场景
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
// ============ 1. 创建 Calendar 实例 ============
Calendar cal = Calendar.getInstance(); // 默认当前时间
Calendar cal2 = Calendar.getInstance();
cal2.setTime(new Date()); // 通过 Date 设置
cal2.setTimeInMillis(System.currentTimeMillis()); // 通过时间戳设置

// ============ 2. 获取字段值 ============
int year = cal.get(Calendar.YEAR); // 2026
int month = cal.get(Calendar.MONTH); // 0~11(0=一月,需 +1)
int day = cal.get(Calendar.DAY_OF_MONTH); // 1~31
int hour = cal.get(Calendar.HOUR_OF_DAY); // 0~23(24小时制)
int hour12 = cal.get(Calendar.HOUR); // 0~11(12小时制)
int minute = cal.get(Calendar.MINUTE); // 0~59
int second = cal.get(Calendar.SECOND); // 0~59
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=周日, 2=周一, ..., 7=周六
int dayOfYear = cal.get(Calendar.DAY_OF_YEAR); // 1~366
int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR); // 1~53

// ⚠️ MONTH 从 0 开始,实际月份需要 +1
int realMonth = cal.get(Calendar.MONTH) + 1; // 1~12

// ============ 3. 设置字段值 ============
cal.set(Calendar.YEAR, 2025);
cal.set(Calendar.MONTH, Calendar.DECEMBER); // 11(可直接用常量)
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); // 2025-12-25
cal.set(2025, Calendar.DECEMBER, 25, 14, 30, 0); // 2025-12-25 14:30:00

// ============ 4. 日期加减(add / roll) ============
// add:会进位/退位(影响更大字段)
cal.add(Calendar.DAY_OF_MONTH, 7); // 加7天
cal.add(Calendar.MONTH, -3); // 减3个月
cal.add(Calendar.YEAR, 1); // 加1年
cal.add(Calendar.HOUR_OF_DAY, -2); // 减2小时

// roll:只改变当前字段,不进位(不影响更大字段)
cal.roll(Calendar.DAY_OF_MONTH, 40); // 加40天但不会进位到下月(如1月20日 → 1月30日)

// ============ 5. Calendar ↔ Date 转换 ============
// Calendar → Date
Date date = cal.getTime();

// Date → Calendar
Calendar cal3 = Calendar.getInstance();
cal3.setTime(date);

// Calendar → 时间戳
long ts = cal.getTimeInMillis();

// ============ 6. 比较 ============
Calendar calA = Calendar.getInstance();
Calendar calB = Calendar.getInstance();
calB.add(Calendar.DAY_OF_MONTH, 1);

calA.before(calB); // true(calA 在 calB 之前)
calA.after(calB); // false
calA.compareTo(calB);// 负数(calA < calB)

// ============ 7. 常用场景 ============
// 场景一:获取当月第一天 00:00:00
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);

// 场景二:获取当月最后一天 23:59:59
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); // 28~31

// 场景四:获取指定日期是星期几(中文)
String[] weekDays = {"周日","周一","周二","周三","周四","周五","周六"};
int weekIndex = cal.get(Calendar.DAY_OF_WEEK) - 1; // 0=周日
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

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
// ============ 1. 创建格式化器 ============
// 自定义格式
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]"));


// 预定义 ISO 格式(无需手写 pattern)
DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 2026-08-05T14:30:00
DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE; // 2026-08-05
DateTimeFormatter basicIso = DateTimeFormatter.BASIC_ISO_DATE; // 20260805

// 含时区/偏移的格式
DateTimeFormatter isoOffset = DateTimeFormatter.ISO_OFFSET_DATE_TIME; // 2026-08-05T14:30:00+08:00

// ============ 2. 格式化(对象 → 字符串) ============
LocalDateTime now = LocalDateTime.now();
String s1 = now.format(fmt); // "2026-08-05 14:30:00"
String s2 = fmt.format(now); // 等价写法,结果相同

// ============ 3. 解析(字符串 → 对象) ============
LocalDateTime t1 = LocalDateTime.parse("2026-08-05 14:30:00", fmt);
LocalDate d1 = LocalDate.parse("2026-08-05", DateTimeFormatter.ISO_LOCAL_DATE);
// 使用默认 ISO 格式时也可省略 formatter
LocalDateTime t2 = LocalDateTime.parse("2026-08-05T14:30:00");

// ============ 4. 指定 Locale(本地化语言/格式) ============
DateTimeFormatter cnFmt = DateTimeFormatter
.ofPattern("yyyy MMMM dd EEEE", Locale.CHINA);
String cn = now.format(cnFmt); // "2026 八月 05 星期三"

// ============ 5. 指定时区(带 ZonedDateTime) ============
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
DateTimeFormatter zonedFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz");
String zStr = zdt.format(zonedFmt); // "2026-08-05 14:30:00 CST"

// ============ 6. 线程安全:定义为常量复用 ============
// 推荐做法:避免每次格式化都 new 一个 formatter
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();                    // → 2026-08-06
LocalDate date = LocalDate.of(2026, 8, 6); // → 2026-08-06
LocalDate parsed = LocalDate.parse("2026-08-06"); // → 2026-08-06

int year = today.getYear();// → 2026
int month = today.getMonthValue(); // → 8(1-12,不是0起始)
int day = today.getDayOfMonth(); // → 6
DayOfWeek dow = today.getDayOfWeek();// → WEDNESDAY

LocalDate tomorrow = today.plusDays(1); // → 2026-08-07
LocalDate lastMonth = today.minusMonths(1); // → 2026-07-06
LocalDate firstDay = today.withDayOfMonth(1); // → 2026-08-01(本月1号)
boolean isLeap = today.isLeapYear(); // → false(2026非闰年)
boolean before = today.isBefore(date); // → false(同一天)

LocalTime 时分秒

1
2
3
4
5
6
7
LocalTime now = LocalTime.now();              // → 14:30:25.123
LocalTime t = LocalTime.of(14, 30, 0); // → 14:30:00

int hour = now.getHour(); // → 14
int min = now.getMinute(); // → 30

LocalTime later = now.plusHours(2).plusMinutes(30); // → 17:00:25.123

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
// ============ 1. 创建实例 ============
LocalDateTime now = LocalDateTime.now(); // 当前日期时间
LocalDateTime specific = LocalDateTime.of(2026, 8, 5, 14, 30, 0); // 2026-08-05 14:30:00
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"));

// ============ 2. 获取字段值 ============
int year = now.getYear(); // 2026
int month = now.getMonthValue(); // 1~12(不再从 0 开始!)
int day = now.getDayOfMonth(); // 1~31
int hour = now.getHour(); // 0~23
int minute = now.getMinute(); // 0~59
int second = now.getSecond(); // 0~59
DayOfWeek dayOfWeek = now.getDayOfWeek(); // MONDAY, TUESDAY, ...

// ============ 3. 日期加减 ============
LocalDateTime plusDays = now.plusDays(7); // 加7天
LocalDateTime minusMonths = now.minusMonths(3); // 减3个月
LocalDateTime plusHours = now.plusHours(2); // 加2小时
LocalDateTime plusYears = now.plusYears(1); // 加1年

// ============ 4. 修改字段 ============
LocalDateTime modified = now.withYear(2025)
.withMonth(12)
.withDayOfMonth(25)
.withHour(14)
.withMinute(30)
.withSecond(0);

// ============ 5. 格式化与解析 ============
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(fmt); // "2026-08-05 14:30:00"
LocalDateTime fromStr = LocalDateTime.parse("2026-08-05 14:30:00", fmt);

// ============ 6. 比较 ============
boolean isBefore = now.isBefore(specific); // true/false
boolean isAfter = now.isAfter(specific);
boolean isEqual = now.isEqual(specific);

// ============ 7. 日期间隔计算 ============
// Duration:时间维度(时、分、秒)
Duration duration = Duration.between(now, specific);
long hours = duration.toHours();
long minutes = duration.toMinutes();

// Period:日期维度(年、月、日)
Period period = Period.between(today, LocalDate.of(2026, 12, 31));
int diffYears = period.getYears();
int diffMonths = period.getMonths();
int diffDays = period.getDays();

// ChronoUnit:直接计算差值
long daysBetween = ChronoUnit.DAYS.between(today, LocalDate.of(2026, 12, 31));

// ============ 8. 与 Date / Calendar / 时间戳互转 ============
// LocalDateTime → Date
Date date = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());

// Date → LocalDateTime
LocalDateTime fromDate = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());

// LocalDateTime → 时间戳(毫秒)
long timestamp = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

// 时间戳 → LocalDateTime
LocalDateTime fromTs = LocalDateTime.ofInstant(
Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());

// Calendar → LocalDateTime
Calendar cal = Calendar.getInstance();
LocalDateTime fromCal = LocalDateTime.ofInstant(cal.toInstant(), ZoneId.systemDefault());

// ============ 9. 常用场景 ============
// 场景一:获取当月第一天 00:00:00
LocalDateTime firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth())
.withHour(0).withMinute(0).withSecond(0).withNano(0);

// 场景二:获取当月最后一天 23:59:59
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(); // 00:00:00
LocalDateTime endOfDay = today.atTime(LocalTime.MAX); // 23:59:59.999999999

Period 年月日间隔

1
2
3
4
5
6
7
// 从 2020-01-01 到 2026-08-06 相差多少
Period p = Period.between(LocalDate.of(2020, 1, 1), LocalDate.of(2026, 8, 6));
int years = p.getYears(); // → 6(相差6年)
int months = p.getMonths(); // → 7(余7个月)
int days = p.getDays(); // → 5(余5天)
// 含义:6年7个月5天。注意各字段是拆分后的余数,不是总数!
// 如果想要"总共多少天",用 ChronoUnit.DAYS.between()

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); // 入参:LocalTime/LocalDateTime/Instant
long hours = d.toHours();// → 8(总小时数)
long minutes = d.toMinutes(); // → 510(总分钟数)
long seconds = d.getSeconds();// → 30600(总秒数)
long millis = d.toMillis(); // → 30600000(总毫秒数)

Duration twoHours = Duration.ofHours(2); // 手动创建 2小时的Duration

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);

// 直接算"总共差多少",比 Period/Duration 更直观
long days = ChronoUnit.DAYS.between(start, end); // → 2409(总天数)
long months = ChronoUnit.MONTHS.between(start, end); // → 79(总月数)
long years = ChronoUnit.YEARS.between(start, end); // → 6(总年数)

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); // → 8
long minutes = ChronoUnit.MINUTES.between(t1, t2); // → 510

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);
// 输出示例:2026-08-31
}
}

DateTimeFormatter 格式化/解析

1
2
3
4
5
6
7
8
9
10
11
12
13
// 线程安全,可做 static final 全局复用(替代 SimpleDateFormat)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

String str = LocalDateTime.now().format(fmt);
// → "2026-08-06 14:30:25"

LocalDateTime dt = LocalDateTime.parse("2026-08-06 14:30:00", fmt);
// → 2026-08-06T14:30:00

// 内置常量(不用自己写pattern)
DateTimeFormatter.ISO_LOCAL_DATE; // 格式: yyyy-MM-dd→ "2026-08-06"
DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 格式: yyyy-MM-ddTHH:mm:ss → "2026-08-06T14:30:00"

Instant 时间戳(机器时间,UTC)

1
2
3
4
5
6
7
8
9
10
11
Instant now = Instant.now();             // → 2026-08-06T06:30:25.123Z(UTC时间)
long epochMilli = now.toEpochMilli(); // → 1786168225123(毫秒时间戳)
Instant ins = Instant.ofEpochMilli(1786168225123L); // 从毫秒创建 → 同上

// Instant → LocalDateTime(需要指定时区)
LocalDateTime ldt = LocalDateTime.ofInstant(ins, ZoneId.systemDefault());
// → 2026-08-06T14:30:25.123(东八区 +8h)

// LocalDateTime → Instant
Instant back = ldt.atZone(ZoneId.systemDefault()).toInstant();
// → 2026-08-06T06:30:25.123Z

ZonedDateTime 带时区

1
2
3
4
5
6
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// → 2026-08-06T14:30:25.123+08:00[Asia/Shanghai]

// 时区转换(同一时刻,不同时区表示)
ZonedDateTime tokyo = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// → 2026-08-06T15:30:25.123+09:00[Asia/Tokyo](日本比中国快1小时)

TemporalAdjusters 日期调节器

1
2
3
4
5
6
7
8
9
10
LocalDate today = LocalDate.of(2026, 8, 6); // 假设今天是周四

LocalDate firstDay = today.with(TemporalAdjusters.firstDayOfMonth());
// → 2026-08-01(本月1号)

LocalDate lastDay = today.with(TemporalAdjusters.lastDayOfMonth());
// → 2026-08-31(本月最后一天)

LocalDate nextMon = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
// → 2026-08-10(下一个周一)

新旧 API 互转

1
2
3
4
5
6
7
8
9
10
11
12
13
// Date → LocalDateTime
Date old = new Date(); // → Thu Aug 06 14:30:25 CST 2026
LocalDateTime ldt = LocalDateTime.ofInstant(old.toInstant(), ZoneId.systemDefault());
// → 2026-08-06T14:30:25

// LocalDateTime → Date
Date newDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// → Thu Aug 06 14:30:25 CST 2026

// long时间戳 → LocalDateTime
long ts = System.currentTimeMillis(); // → 1786168225123
LocalDateTime fromTs = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault());
// → 2026-08-06T14:30:25.123

注意事项:

要点 说明
线程安全 不可变对象,可放心定义为 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};

// toString:数组转字符串(直接打印数组是地址值,需用此方法)
String s = Arrays.toString(arr); // → "[3, 1, 4, 1, 5, 9, 2, 6]"

// sort:排序(原地排序,改变原数组,无返回值)
Arrays.sort(arr); // arr 变为 {1, 1, 2, 3, 4, 5, 6, 9}

// sort 降序写法(用包装类 + Collections.reverseOrder)
Integer[] arrBox = {3, 1, 4, 1, 5, 9, 2, 6};
Arrays.sort(arrBox, Collections.reverseOrder()); // → {9, 6, 5, 4, 3, 2, 1, 1}

// sort 匿名内部类写法(自定义排序规则) - 要用包装类Integer而不是int
Arrays.sort(arrBox, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1; // 降序(o1-o2 为升序)
}
}); // → {9, 6, 5, 4, 3, 2, 1, 1}

// sort 部分排序(只排 [fromIndex, toIndex) 区间)
int[] part = {5, 3, 1, 4, 2};
Arrays.sort(part, 0, 3); // → {1, 3, 5, 4, 2}(只排前3个)

// binarySearch:二分查找(必须先排序!)
int idx = Arrays.binarySearch(arr, 5); // → 5(返回下标,找不到返回 -插入点-1)

// copyOf:从开头复制指定长度(新数组)
int[] copy = Arrays.copyOf(arr, 3); // → {1, 1, 2}(只取前3个)
int[] padded = Arrays.copyOf(arr, 10); // → {1,1,2,3,4,5,6,9,0,0}(不足补0)

// copyOfRange:复制指定区间 [from, to)(左闭右开)
int[] range = Arrays.copyOfRange(arr, 2, 5); // → {2, 3, 4}

// fill:填充
int[] empty = new int[5];
Arrays.fill(empty, 7); // → {7, 7, 7, 7, 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))); // 要重写sort

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"));

// 1. 普通 for-each(最常用,简单安全)
for (String s : c) {
System.out.println(s);
}

// 2. 迭代器 Iterator(遍历中可安全删除)
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String s = it.next();
if ("b".equals(s)) it.remove(); // 用迭代器删除,避免 ConcurrentModificationException
}

// 3. forEach + lambda(Java 8+,简洁)
c.forEach(s -> System.out.println(s));
// 方法引用简写
c.forEach(System.out::println);

// 4. 转数组后遍历
String[] arr = c.toArray(new String[0]);
for (String s : arr) { System.out.println(s); }

// 5. forEach + 匿名内部类 new Consumer(等价于 lambda,便于理解函数式接口本质)
c.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// 本质上 c.forEach(s -> System.out.println(s)) 就是上面匿名内部类的 lambda 简写

遍历方式选择建议

方式 适用场景
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.*;

// ========== ArrayList(底层数组,查询快O(1),增删慢O(n)) ==========
List<String> arrayList = new ArrayList<>();
arrayList.add("A"); // 尾部添加
arrayList.add(0, "B"); // 指定位置插入 → [B, A]
arrayList.get(0); // 按索引取值 "B"
arrayList.set(0, "C"); // 替换 → [C, A]
arrayList.remove(0); // 按索引删除
arrayList.remove("A"); // 按对象删除(删第一个匹配)
arrayList.contains("C"); // 是否包含
arrayList.indexOf("C"); // 首次出现索引(-1=不存在)
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.remove只会删除一个(同值时)
List<String> list = new ArrayList<>(Arrays.asList("aaa", "bbb", "aaa"));
list.remove("aaa");
System.out.println(list);
// 输出结果:[bbb, aaa] (只删除了第一个 aaa)


// 删除多个重复?
list.removeAll(Collections.singleton("aaa"));
// 或者 list.removeAll(Arrays.asList("aaa"));
// 结果:list 变为 [bbb]
list.removeIf(s -> s.equals("aaa")); (Java 8+,最灵活)
// 结果:list 变为 [bbb]



// remove陷阱
List<Integer> numbers = new ArrayList<>(Arrays.asList(10, 20, 30));

numbers.remove(1); // 删除的是索引 1,即删除 20
// 结果:[10, 30]

numbers.remove(Integer.valueOf(1)); // 删除的是值为 1 的元素(如果存在)


// ========== LinkedList(底层双向链表,增删快O(1),查询慢O(n)) ==========
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A"); // 尾部添加
linkedList.addFirst("B"); // 头部添加
linkedList.addLast("C"); // 尾部添加
linkedList.getFirst(); // 获取头部 "B"
linkedList.getLast(); // 获取尾部 "C"
linkedList.removeFirst(); // 删除头部
linkedList.removeLast(); // 删除尾部
linkedList.peek(); // 查看头部(不删除,空返回null)
linkedList.poll(); // 取出头部(删除,空返回null)
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); // [1, 2, 3, 100, 4]
}

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.*;

// ========== HashSet(底层HashMap,无序,增删查O(1)) ==========
Set<String> hashSet = new HashSet<>();
hashSet.add("A"); // 添加
hashSet.add("B");
hashSet.add("A"); // 重复元素,不会添加
hashSet.contains("A"); // true
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); // 并集 [A, B, C, D]

Set<String> inter = new HashSet<>(s1);
inter.retainAll(s2); // 交集 [B, C]

Set<String> diff = new HashSet<>(s1);
diff.removeAll(s2); // 差集 [A]

// ========== TreeSet(底层红黑树,元素有序,增删查O(log n)) ==========
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(1);
treeSet.add(3);
// 自动排序:[1, 3, 5]

treeSet.first(); // 最小元素 1
treeSet.last(); // 最大元素 5
treeSet.lower(3); // 小于3的最大元素 → 1
treeSet.higher(3); // 大于3的最小元素 → 5
treeSet.floor(3); // ≤3 的最大元素 → 3
treeSet.ceiling(3); // ≥3 的最小元素 → 3
treeSet.headSet(3); // 小于3的子集 [1]
treeSet.tailSet(3); // ≥3 的子集 [3, 5]
treeSet.subSet(1, 5); // [1, 5) 范围子集 [1, 3]
treeSet.pollFirst(); // 取出并删除最小
treeSet.pollLast(); // 取出并删除最大

// 自定义排序
TreeSet<String> customSet = new TreeSet<>(Comparator.comparing(String::length));
customSet.addAll(Arrays.asList("apple", "hi", "cat"));
// 按长度排序:[hi, cat, apple]

// ========== LinkedHashSet(保持插入顺序的HashSet) ==========
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("C");
linkedSet.add("A");
linkedSet.add("B");
// 遍历顺序始终为:C, A, 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(有序、可重复、索引访问)=====
list.add("A"); // 尾部添加
list.add(0, "B"); // 指定位置插入 → [B, A]
list.get(0); // 按索引取 → "B"(链表要遍历,O(n))
list.set(0, "C"); // 替换 → [C, A]
list.remove(0); // 按索引删除
list.remove("A"); // 按对象删除
list.contains("C"); // 是否包含
list.size(); // 元素个数

// ===== 作为 Queue(FIFO,常用)=====
list.offer("X"); // 入队(队尾)
list.poll(); // 出队(队头,空返回 null)
list.peek(); // 看队头(不删除,空返回 null)

// ===== 作为 Deque / 栈(LIFO)=====
list.addFirst("Y"); // 头部添加
list.addLast("Z"); // 尾部添加
list.getFirst(); // 取头部
list.getLast(); // 取尾部
list.removeFirst(); // 删头部
list.removeLast(); // 删尾部
list.push("P"); // 压栈(= addFirst)
list.pop(); // 弹栈(= removeFirst)

常用场景

场景 推荐方法 说明
频繁在头部/中间增删 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));
// 结果:[a, b, c](顺序不保证)

// 去重并保持原顺序(推荐用 LinkedHashSet)
List<String> ordered = new ArrayList<>(new LinkedHashSet<>(list));
// 结果:[a, b, c](按首次出现顺序)

适用需求:接口返回列表去重、Excel 批量导入剔除重复行、日志关键字去重展示。

2. 快速判断「是否已存在 / 是否重复」(O(1))

1
2
3
4
5
6
Set<String> seen = new HashSet<>();
for (String item : items) {
if (!seen.add(item)) { // add 返回 false 表示已存在 → 重复
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
// 用 HashSet 充当"已处理ID"缓存,避免消息被重复消费
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); // 并集 [A,B,C,D]
Set<String> inter = new HashSet<>(s1); inter.retainAll(s2); // 交集 [B,C]
Set<String> diff = new HashSet<>(s1); diff.removeAll(s2); // 差集 [A]

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
// 自然排序(元素需实现 Comparable)
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(5); treeSet.add(1); treeSet.add(3);
// 遍历顺序始终 [1, 3, 5]

// 自定义排序(如按字符串长度)
TreeSet<String> byLen = new TreeSet<>(Comparator.comparing(String::length));
byLen.addAll(Arrays.asList("apple", "hi", "cat"));
// 遍历顺序 [hi, cat, apple]

0. 两种排序方式(核心)

TreeSet 的排序只来自两个来源:元素自身实现 Comparable 或 **构造时传入 Comparator**。两者二选一,同时传 Comparator 时以它为准。

① 自然排序(元素实现 Comparable)

1
2
3
4
5
6
7
8
// 元素类实现 Comparable<T>,重写 compareTo 定义升序规则
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,按 compareTo 排序

② 定制排序(构造时传 Comparator,不用改元素类)

1
2
3
4
5
6
7
8
9
// 倒序
TreeSet<Integer> desc = new TreeSet<>(Comparator.reverseOrder());
desc.addAll(Arrays.asList(3, 1, 2)); // [3, 2, 1]

// 多字段排序(先分数降序,再年龄升序)
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));
// 结果:[1, 2, 3]

适用需求:排行榜去重排序、统计指标去重后输出有序集合、按权重/分数排序的标签列表。

2. 范围查询 / 取最值(红黑树的优势)

1
2
3
4
5
6
7
8
9
10
11
12
13
treeSet.first();      // 最小元素
treeSet.last(); // 最大元素
treeSet.lower(3); // 小于 3 的最大元素
treeSet.higher(3); // 大于 3 的最小元素
treeSet.floor(3); // ≤ 3 的最大元素
treeSet.ceiling(3); // ≥ 3 的最小元素

treeSet.headSet(3); // 小于 3 的子集(视图)
treeSet.tailSet(3); // ≥ 3 的子集(视图)
treeSet.subSet(1, 5); // [1, 5) 区间子集(视图)

treeSet.pollFirst(); // 取出并删除最小元素
treeSet.pollLast(); // 取出并删除最大元素

适用需求:价格区间筛选、时间窗口内数据截取、TopN / 末位淘汰、延迟队列取最早到期任务。

3. 自定义对象排序(必须提供 Comparator 或实现 Comparable)

1
2
3
4
5
// 按用户年龄排序去重(TreeSet 用 Comparator 判重+排序)
TreeSet<User> users = new TreeSet<>(Comparator.comparingInt(User::getAge));
users.add(new User("张三", 20));
users.add(new User("李四", 18));
// 遍历顺序:李四(18) → 张三(20)

⚠️ 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<>(); // key 自动排序

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("安徽", "省会合肥") // 第 11 个
));

// ✅ 后续新增完全没问题
map.put("北京", "首都");
map.put("上海", "直辖市");

// ✅ 修改已有的也没问题(覆盖掉“省会成都”)
map.put("四川", "天府之国");

// 验证输出
System.out.println(map.get("北京")); // 输出:首都
System.out.println(map.get("四川")); // 输出:天府之国
System.out.println(map.size()); // 输出:13(原来11个 + 新增2个)

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
        // 初始化几个map,在构建时指定
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); // 存(key 已存在则覆盖)
map.put("b", 2); put修改-》返回原本的值,新增-》返回 null
map.get("a"); // 取 → 1(key 不存在返回 null)
map.getOrDefault("c", 0); // 没有属于C时,使用默认值
map.containsKey("a"); // 是否含 key → true
map.containsValue(2); // 是否含 value → true
map.remove("b"); // 删
map.size(); // 元素个数
map.putIfAbsent("a", 9); // key 不存在才存(已存在则不动)

// ===== 遍历(三种常用方式)=====
// 方式一:entrySet() 遍历键值对 —— 最常用、最高效(一次拿到 key + value,不二次查表)
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}

// 方式二:keySet() 遍历 key —— 只需 key 时可用,取 value 要多一次 get(大 Map 略慢)
for (String key : map.keySet()) {
System.out.println(key + "=" + map.get(key));
}

// 方式三:values() 遍历 value —— 只关心值、不要 key 时用
for (Integer val : map.values()) {
System.out.println(val);
}

// 方式四(JDK8+):forEach + lambda —— 写法最简洁,本质仍是 entrySet
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);
}
// h 遍历顺序:c, a, b(哈希决定,不保证)
// l 遍历顺序:c, a, b(插入顺序,不受哈希扰动)
// t 遍历顺序:a, b, c(key 自然升序)

适用需求:

  • 只存键值、不关心顺序(缓存、字典、计数)→ HashMap
  • 需要按放入顺序输出(导出 Excel 保持列顺序、配置按定义顺序展示)→ LinkedHashMap
  • 需要 key 排序输出(按字母/数字有序、区间查询)→ TreeMap

3. LinkedHashMap 实现 LRU 缓存(访问顺序)

LinkedHashMap 构造时可开启 accessOrder=true,每次 get 会把元素移到队尾,配合 removeEldestEntry 即可实现最近最少使用(LRU)淘汰。

1
2
3
4
5
6
7
8
9
10
// 容量上限 3 的 LRU 缓存:超过则淘汰最久未访问的
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"); // 访问 a,a 移到末尾 → 顺序 b, c, a
lru.put("d", 4); // 超限,淘汰最久未访问的 b → 剩 c, a, d

适用需求:本地缓存、历史记录(保留最近 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(); // 最小 key → 1
tm.lastKey(); // 最大 key → 3
tm.lowerKey(2); // < 2 的最大 key → 1
tm.higherKey(2); // > 2 的最小 key → 3
tm.floorKey(2); // ≤ 2 的最大 key → 2
tm.ceilingKey(2); // ≥ 2 的最小 key → 2
tm.subMap(1, 3); // [1, 3) 区间的子 Map(视图)
tm.headMap(2); // < 2 的子 Map
tm.tailMap(2); // ≥ 2 的子 Map

适用需求:按 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;
// HashMap 做 key:必须重写 hashCode 和 equals
@Override public int hashCode() { return Objects.hash(id); }
@Override public boolean equals(Object o) { return o instanceof User && Objects.equals(id, ((User) o).id); }
}
// TreeMap 做 key:User 需实现 Comparable 或构造时传 Comparator

⚠️ 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);
}
// JDK8 更简洁
list.forEach(s -> count.merge(s, 1, Integer::sum));

// 按某字段分组(值可能是 List)
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;

// ===== 栈(LIFO)=====
ArrayDeque<String> stack = new ArrayDeque<>();
stack.push("1"); // 压栈 → [1]
stack.push("2"); // 压栈 → [2, 1] (栈顶是 2)
stack.push("3"); // 压栈 → [3, 2, 1]
stack.peek(); // 看栈顶 → "3"
stack.pop(); // 弹栈 → "3",剩余 [2, 1]

// ===== 队列(FIFO)=====
ArrayDeque<String> queue = new ArrayDeque<>();
queue.offer("1"); // 入队 → [1]
queue.offer("2"); // 入队 → [1, 2]
queue.offer("3"); // 入队 → [1, 2, 3]
queue.peek(); // 看队头 → "1"
queue.poll(); // 出队 → "1",剩余 [2, 3] (先来的先走)

⚠️ 注意:Java 自带的 java.util.Stack 类已过时(继承自 Vector,方法同步、性能差),推荐用 ArrayDeque 实现栈和队列。


泛型(Generics)

泛型是 JDK 5 引入的特性,作用是在编译期做类型检查、避免强制类型转换,把”运行时才暴露的类型错误”提前到编译时。核心符号是 <T>(Type 的占位符,T 可换成任意大写字母)。

0. 为什么需要泛型?(强转的痛点)

在泛型出现之前,集合只能存 Object,取出来必须手动强转,而且编译期不检查类型,错误要到运行时才爆。

1
2
3
4
5
6
7
8
// ===== 没有泛型的写法(JDK 5 之前 / raw type)=====
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译器不拦,什么都能塞进去

// 取出来必须强转,否则编译报错
String s1 = (String) list.get(0); // ✅ 运行正常
String s2 = (String) list.get(1); // ❌ 运行时 ClassCastException:Integer 不能转 String

三个痛点:

  1. 强转繁琐:每次 get 都要 (类型) 强转,代码啰嗦
  2. 编译期不检查:塞错类型编译器无感知,错误延迟到运行时才暴露(生产环境才崩)
  3. 类型不安全:一个 List 里混了多种类型,取的时候根本不知道该转成什么

用了泛型之后:

1
2
3
4
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // ❌ 编译期直接报错,从源头杜绝错误
String s = list.get(0); // ✅ 免强转,编译器已知是 String

对比一目了然:泛型把强转和类型检查交给编译器,既省掉 (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
// 定义一个泛型容器类,T 是“未知类型占位符”
public class Box<T> {
private T value;

public void set(T value) { // 入参类型 T
this.value = value;
}
public T get() { // 返回值类型 T
return value;
}
}

// 使用时指定具体类型,编译器会做类型检查
Box<String> strBox = new Box<>(); // JDK 7+ 可省略右边 <>,菱形语法
strBox.set("hello");
String s = strBox.get(); // 不需要 (String) 强转

Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer n = intBox.get();

// strBox.set(123); // ❌ 编译报错:要求 String,给了 Integer

对比 raw type(需要强转):如果不用泛型(裸类型),get() 返回 Object,必须手动强转,否则编译报错:

1
2
3
4
Box rawBox = new Box();          // 没写 <String>,裸类型
rawBox.set("hello");
// String s = rawBox.get(); // ❌ 编译报错:Object 不能直接赋给 String
String s = (String) rawBox.get(); // ✅ 必须 (String) 强转(运行时若不是 String 会 ClassCastException)

加了 <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
// 泛型接口:能存一个 T 类型的东西
public interface Holder<T> {
void put(T item); // 存一个 T
T get(); // 取一个 T
}

// ===== 方式一:把 T 定死成 String =====
// 这个盒子"出厂就只认 String",想存 Integer 就得再写个 IntegerHolder
public class StringHolder implements Holder<String> {
private String value;
public void put(String item) { value = item; } // T 已被替换成 String
public String get() { return value; }
}

StringHolder h1 = new StringHolder();
h1.put("hello");
String s = h1.get(); // 直接拿到 String,免强转
// h1.put(123); // ❌ 报错:这个盒子只收 String


// ===== 方式二:实现类自己继续带 <T> =====
// 这个盒子"不挑类型",用的时候由你决定 T 是 String 还是 Integer
public class AnyHolder<T> implements Holder<T> {
private T value;
public void put(T item) { value = item; } // T 由使用方决定
public T get() { return value; }
}

AnyHolder<String> h2 = new AnyHolder<>();
h2.put("hello");
String s2 = h2.get(); // T = String

AnyHolder<Integer> h3 = new AnyHolder<>();
h3.put(123);
Integer n = h3.get(); // 同一个类,T 换成 Integer 照样用

一句话区分:

  • 方式一 = 这个实现”专一”,只服务一种类型(适合固定业务对象)。
  • 方式二 = 这个实现”通用”,谁用谁定类型(适合写工具类)。

💡 日常最常见的泛型接口其实是 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 {

// 泛型方法:<T> 在 static/返回值之前
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);
}

// 有上界约束:T 必须是 Number 或其子类
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T t : list) {
total += t.doubleValue(); // 因为 T extends Number,所以能调 doubleValue()
}
return total;
}
}

// 调用:类型由实参自动推断,无需手写 <String>
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
// 上界:? extends T —— 只能读,不能写(Producer 生产者)
? super T 类型是 T 或 T 的父类(下限)
// 表示?可以是任何类型,但是一定要继承了Number
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); // ✅ Integer
getData(456); // ✅ Integer(没有子类,因为 Integer 是 final 类)



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 不可变
List<String> list = List.of("a", "b", "c");
list.add("d"); // ❌ UnsupportedOperationException
list.set(0, "x"); // ❌ UnsupportedOperationException
list.get(0); // ✅ "a"
// Java 8 等效写法
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b", "c"));

// Set 不可变(自动去重,元素不可重复)
Set<Integer> set = Set.of(1, 2, 3);
set.add(4); // ❌ UnsupportedOperationException
// Java 8 等效写法
Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

// Map 不可变
Map<String, Integer> map = Map.of("a", 1, "b", 2); // 最多 10 个键值对 Map.copyof 和Map.of一样
Map<String, Integer> map2 = Map.ofEntries( // 超过 10 对用 ofEntries
Map.entry("a", 1),
Map.entry("b", 2)
);
map.put("c", 3); // ❌ UnsupportedOperationException
map.get("a"); // ✅ 1

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"); // ❌ UnsupportedOperationException(视图不让改)

src.add("c"); // ✅ 原集合仍能改
System.out.println(readOnly); // [a, c] —— 视图跟着原集合变(不是真正冻结)

关键点:这是视图(view)不是副本。要真正冻结数据,应先 new ArrayList<>(src) 拷一份再包装。

3. Guava 的不可变集合(推荐做”真·常量”)

Guava 提供 ImmutableList / ImmutableSet / ImmutableMap,创建后完全冻结(连底层都改不了),且允许 null(会直接报错,更安全)。

1
2
3
4
5
6
7
8
9
10
11
12
// 方式一:of 直接给元素
ImmutableList<String> list = ImmutableList.of("a", "b", "c");

// 方式二:Builder(元素多时清晰)
ImmutableMap<String, Integer> map = ImmutableMap.<String, Integer>builder()
.put("a", 1)
.put("b", 2)
.build();

// 方式三:copyOf 把已有集合拷贝成不可变(最常用,防御性拷贝)
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); // 自然升序 → [1, 2, 3]
Collections.sort(list, Comparator.reverseOrder()); // 降序
Collections.reverse(list); // 反转顺序(元素倒过来)
Collections.shuffle(list); // 随机打乱(洗牌)
Collections.rotate(list, 2); // 循环右移 2 位:把末尾 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); // 最大值 → 5
int min = Collections.min(list); // 最小值 → 1
int idx = Collections.binarySearch(list, 2); // 二分查找(前提:list 已排序!)
// 找到返回索引,找不到返回 (-(插入点) - 1)
int cnt = Collections.frequency(list, 2); // 从list中统计2出现的次数

binarySearch 必须先在已排序的 List 上用,否则结果无意义。

3. 填充 / 替换 / 拷贝(List)

1
2
3
4
5
6
List<String> list = new ArrayList<>(Collections.nCopies(3, "x")); // 初始 3 个 "x" → [x, x, x]

Collections.fill(list, "y"); // 全部替换为 "y" → [y, y, y]
Collections.replaceAll(list, "y", "z"); // 把所有 "y" 换成 "z"
Collections.copy(dest, src); // 把 src 拷到 dest(dest 长度需 ≥ src,只覆盖前 src.size() 个)
Collections.swap(list, 0, 2); // 交换索引 0 和 2 的元素

Collections.nCopies(n, obj) 生成的 List 不可变,常用于 fill 前的初始化。

4. 不可变包装(只读视图)

1
2
3
4
List<String> readOnly = Collections.unmodifiableList(srcList);   // 只读 List
Set<Integer> roSet = Collections.unmodifiableSet(srcSet); // 只读 Set
Map<String,Integer> roMap = Collections.unmodifiableMap(srcMap); // 只读 Map
// 视图上调用 add/remove/put 等 → UnsupportedOperationException

这是视图不是副本:底层原集合仍可改,视图会跟着变(详见上一节”不可变集合”坑点)。

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"); // 只含 1 个元素,不可变   jdk9推荐用list.of
Set<Integer> one = Collections.singleton(9); // 单元素 Set,不可变
List<String> empty = Collections.emptyList(); // 共享的空 List(不可变,省内存)
Map<String,Integer> emptyMap = Collections.emptyMap(); // 空 Map

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类型
list.stream().forEach(s=>{})

// Map类型
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);

// 普通数组 stream + filter 后还原回普通数组返回
int[] filtered = Arrays.stream(arr)
.filter(n -> n % 2 == 0)
.toArray(); // 返回 int[]

// String数组同理
String[] names = {"Alice", "Bob", "Anna"};
String[] result = Arrays.stream(names)
.filter(s -> s.startsWith("A"))
.toArray(String[]::new); // 返回 String[]





// 复杂数据类型 ArrayList<Student> stream + filter 后还原
// 方式一:还原回 List<Student>(最常用)
List<Student> students = new ArrayList<>();
List<Student> passed = students.stream()
.filter(s -> s.getScore() >= 60)
.collect(Collectors.toList()); // 返回 List<Student>

// 方式二:还原回 ArrayList<Student>(Collectors.toCollection 指定容器)
ArrayList<Student> passedList = students.stream()
.filter(s -> s.getScore() >= 60)
.collect(Collectors.toCollection(ArrayList::new));

// 方式三:还原回 Student[] 数组
Student[] passedArr = students.stream()
.filter(s -> s.getScore() >= 60)
.toArray(Student[]::new);

// 方式四:只取某个字段,还原成 List<String>
List<String> names = students.stream()
.filter(s -> s.getScore() >= 60)
.map(Student::getName)
.collect(Collectors.toList()); // 返回 List<String>

// 零散数据
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) // 每个 Order 转成其 userName
.collect(Collectors.toList());

// 数值乘以 2
List<Integer> doubled = nums.stream()
.map(n -> n * 2)
.collect(Collectors.toList()); // → [2, 4, 6, 8, 10, 12]

综合 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
// 直接用普通字符串数组,元素形如 "name-age"
List<String> people = Arrays.asList("xiaoming-22", "xiaohong-30", "xiaoli-55", "xiaowang-62", "xiaozhao-48");

// 1)先把 "name-age" 按 "-" 拆出 age,再 filter age > 50,收集年龄列表
List<Integer> ages = people.stream()
.map(s -> Integer.parseInt(s.split("-")[1])) // 拆出 age:22, 30, 55, 62, 48
.filter(age -> age > 50) // 只留 >50:55, 62
.collect(Collectors.toList()); // → [55, 62]

// 2)等价写法:先 filter 再 map(先筛再转,减少后续处理量)
List<Integer> ages2 = people.stream()
.filter(s -> Integer.parseInt(s.split("-")[1]) > 50) // 先按 age 筛:xiaoli-55, xiaowang-62
.map(s -> Integer.parseInt(s.split("-")[1])) // 再取 age:55, 62
.collect(Collectors.toList()); // → [55, 62]

// 3)收集成 "name -> age" 的 Map
Map<String, Integer> over50 = people.stream()
.filter(s -> Integer.parseInt(s.split("-")[1]) > 50)
.collect(Collectors.toMap(
s -> s.split("-")[0], // key:name(xiaoli, xiaowang)
s -> Integer.parseInt(s.split("-")[1]) // value:age(55, 62)
));
// → {xiaoli=55, xiaowang=62}

// 4)在 collect 中返回自定义类 Student(含 name、age 属性)
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// getter / setter 省略
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()); // → [Student(xiaoli,55), Student(xiaowang,62)]

// 等价紧凑写法
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")
);
// 二维 → 一维:["a","b","c"]
List<String> flat = nested.stream()
.flatMap(List::stream) // 每个内层 List 展开成流
.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()); // 去重 → [3,1,2,5]
data.stream().sorted().collect(Collectors.toList()); // 升序 → [1,2,2,3,3,5]
data.stream().sorted(Comparator.reverseOrder()).collect(...); // 降序
data.stream().limit(3).collect(Collectors.toList()); // 取前 3 个 → [3,1,3]
data.stream().skip(2).collect(Collectors.toList()); // 跳过前 2 个
// 组合:分页(跳过 offset 取 limit)
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();          // 计数 → 3
Optional<Integer> max = nums.stream().max(Integer::compareTo); // 最大值 → 6
Optional<Integer> min = nums.stream().min(Integer::compareTo); // 最小值 → 1

// 求和 / 平均值:先 mapToInt 转基本类型流
int sum = nums.stream().mapToInt(Integer::intValue).sum(); // 总和 → 21
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
// stream转array
String[] list = list.stream().toArray(s->new String[s])

// 转 List / Set
List<String> l = list.stream().collect(Collectors.toList());
Set<String> st = list.stream().collect(Collectors.toSet());

// 转 Map:key=名字, value=对象(需保证 key 不重复,否则抛异常)
Map<String, Order> map = orders.stream()
.collect(Collectors.toMap(Order::getId, o -> o));

// 分组(按某字段分桶,等价于 SQL group by)
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); // 任一满足 → true
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);

// 求最大值 (等价于 numbers.stream().max(Integer::compareTo))
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println); // 输出 9

// 求乘积
Optional<Integer> product = numbers.stream().reduce((a, b) -> a * b);
System.out.println(product.orElse(1)); // 输出 135 (3*5*1*9)



/* -------------有初始值 ------------*/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

// 求和 (初始值 0)
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum); // 输出 10

// 字符串拼接 (初始值 "Hello: ")
List<String> words = Arrays.asList("Java", "Stream", "Reduce");
String result = words.stream().reduce("结果: ", (a, b) -> a + " | " + b);
System.out.println(result); // 输出 "结果: | Java | Stream | Reduce"
// 注意:初始值会直接参与首次计算,所以这里多了一个 " | "


/** --- 有初始值 和 没初始值 结果不同 **/
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());
/** 输出:
结果: | Java | Stream | Reduce
Java | Stream | Reduce


**/

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");

// Lambda 写法
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
// forEach 需要 Consumer<String>:void accept(String s)
// println 正好能接收 String,并且无返回值,所以能匹配
list.forEach(System.out::println);

// map 需要 Function<String, Integer>:Integer apply(String s)
// Integer.parseInt(String s) 正好接收 String,返回 Integer,所以能匹配
List<Integer> nums = Arrays.asList("1", "2", "3").stream()
.map(Integer::parseInt)
.collect(Collectors.toList()); // [1, 2, 3]

2. 从 Lambda 改成方法引用的判断标准

如果 Lambda 只是原样调用一个现成方法,就可以改成方法引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 可以改:参数原样传给 println
s -> System.out.println(s)
System.out::println

// 可以改:参数原样传给 parseInt
s -> Integer.parseInt(s)
Integer::parseInt

// 可以改:参数对象自己调用 length
s -> s.length()
String::length

// 不适合改:中间多了额外处理 toUpperCase()
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");

// Lambda:字符串转数字
List<Integer> nums1 = list.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());

// 方法引用:Integer::parseInt
List<Integer> nums2 = list.stream()
.map(Integer::parseInt)
.collect(Collectors.toList()); // [1, 2, 3]

常见开发写法:

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());

// 过滤非 null
List<String> names = Arrays.asList("tom", null, "jack").stream()
.filter(Objects::nonNull)
.collect(Collectors.toList()); // [tom, jack]

// 过滤 null
List<String> nullList = Arrays.asList("tom", null, "jack").stream()
.filter(Objects::isNull)
.collect(Collectors.toList()); // [null]

// reduce 求和:Integer::sum 等价于 (a, b) -> Integer.sum(a, b)
Integer total = Arrays.asList(1, 2, 3).stream()
.reduce(0, Integer::sum); // 6

5. 指定对象的实例方法引用:对象::实例方法

这种写法中,:: 左边是一个已经存在的对象,不是类名。

1
2
3
4
5
6
7
List<String> list = Arrays.asList("a", "b", "c");

// Lambda
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()); // ABC

// 假设 log 是当前类中的日志对象
// list.forEach(log::info); // 等价于 s -> log.info(s)

List<String> result = new ArrayList<>();
Arrays.asList("x", "y").forEach(result::add); // 等价于 s -> result.add(s)

注意:对象::方法 的对象是固定的,比如 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 ");

// Lambda:每个字符串自己调用 trim()
List<String> trim1 = list.stream()
.map(s -> s.trim())
.collect(Collectors.toList());

// 方法引用:String::trim
List<String> trim2 = list.stream()
.map(String::trim)
.collect(Collectors.toList()); // [apple, banana, pear]

更多常用写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<String> words = Arrays.asList("banana", "apple", "pear");

// 取长度:s -> s.length()
List<Integer> lenList = words.stream()
.map(String::length)
.collect(Collectors.toList()); // [6, 5, 4]

// 转大写:s -> s.toUpperCase()
List<String> upperList = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList()); // [BANANA, APPLE, PEAR]

// 忽略大小写排序:(a, b) -> a.compareToIgnoreCase(b)
words.sort(String::compareToIgnoreCase);

类名::实例方法 的参数对应关系:

1
2
3
4
5
6
7
// 一个参数:Function<String, Integer>
String::length
// 等价于:s -> s.length()

// 两个参数:Comparator<String>
String::compareToIgnoreCase
// 等价于:(a, b) -> a.compareToIgnoreCase(b)

关键点:**String::length 不是调用静态方法,而是让传进来的 String 对象自己调 length()。**

7. 构造方法引用:类名::new

当 Lambda 的作用只是 new 一个对象时,可以用构造方法引用。

1
2
3
4
5
6
// 无参构造:Supplier<ArrayList<String>>
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);

// Lambda:id -> new User(id)
List<User> users1 = ids.stream()
.map(id -> new User(id))
.collect(Collectors.toList());

// 方法引用:User::new,自动匹配 User(Integer id) 构造方法
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<T, U, R>:接收两个参数 T、U,返回一个结果 R
// 这里表示:接收 orderNo 和 amount,创建 OrderDTO 对象
BiFunction<String, BigDecimal, OrderDTO> creator1 =
(orderNo, amount) -> new OrderDTO(orderNo, amount); // Lambda 写法

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[],还要强转
Object[] arr1 = list.stream().toArray();

// 推荐:返回 String[]
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");

// Lambda
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);

// Lambda 写法:筛选奇数
List<Integer> oddList1 = nums.stream()
.filter(num -> NumberFilter.isOdd(num))
.collect(Collectors.toList()); // [1, 3, 5]

// 方法引用写法:类名::静态方法
List<Integer> oddList2 = nums.stream()
.filter(NumberFilter::isOdd)
.collect(Collectors.toList()); // [1, 3, 5]

// 方法引用写法:筛选偶数
List<Integer> evenList = nums.stream()
.filter(NumberFilter::isEven)
.collect(Collectors.toList()); // [2, 4, 6]

关键点: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) // 等价于 num -> checker.isEven(num)
.collect(Collectors.toList()); // [2, 4, 6]

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<>();

// Lambda
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) // 等价于 u -> u.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
// key = 订单号,value = 订单对象
Map<String, Order> orderMap = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Function.identity()));

// key = 订单号,value = 金额
Map<String, BigDecimal> amountMap = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Order::getAmount));

// key 重复时指定覆盖策略:保留新值
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); // 6

int totalLength = Arrays.asList("aa", "bbb", "c").stream()
.mapToInt(String::length)
.sum(); // 6

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");

// 取出 age > 50 的年龄列表
List<Integer> ages = people.stream()
.map(s -> s.split("-")[1]) // 先取出 age 字符串
.map(Integer::parseInt) // 方法引用:String -> Integer
.filter(age -> age > 50)
.collect(Collectors.toList()); // [55, 62]

// 收集成 name -> age 的 Map
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])
)); // {xiaoli=55, xiaowang=62}

这里 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());

// 推荐 Lambda:有额外处理
orders.stream()
.map(o -> "NO-" + o.getOrderNo())
.collect(Collectors.toList());

14. 常见坑与解决办法

14.1 签名必须匹配
1
2
3
4
5
// map 需要 Function<T, R>:接收一个参数,返回一个结果
// Integer::parseInt 接收 String 返回 int,能匹配
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> 推断出 print(String)
Consumer<String> c1 = printer::print;

// 如果上下文不明确,改用 Lambda 指定类型
Consumer<String> c2 = s -> printer.print(s);
14.3 类名::实例方法 不是静态方法调用
1
2
3
4
5
6
String::length
// 等价于 s -> s.length()
// 不是 String.length()

String::compareToIgnoreCase
// 等价于 (a, b) -> a.compareToIgnoreCase(b)
14.4 toMap 的 key 重复会报错
1
2
3
4
5
6
7
8
9
10
11
// 如果 orderNo 重复,会抛 IllegalStateException
Map<String, Order> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Function.identity()));

// 正确:指定重复 key 的合并策略
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());

// 如果逻辑复杂,Lambda 更清楚
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.*;

// ========== 1. 基础操作(java.io.File) ==========

File file = new File("D:/data/test.txt");
File dir = new File("D:/data/subdir");

// 判断
file.exists(); // 文件/目录是否存在
file.isFile(); // 是否是文件
file.isDirectory(); // 是否是目录

// 获取信息
file.getName(); // "test.txt"
file.getPath(); // "D:/data/test.txt"(构造时传入的路径)
file.getAbsolutePath(); // 绝对路径
file.getParent(); // "D:/data"
file.length(); // 文件大小(字节),目录返回0

// 创建
file.createNewFile(); // 创建文件(父目录必须存在),已存在返回false
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对象数组
// 带过滤
File[] txts = parent.listFiles((d, name) -> name.endsWith(".txt"));

// ========== 2. NIO.2(java.nio.file,推荐) ==========

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); // Java 11+
List<String> lines = Files.readAllLines(path); // 按行读
byte[] bytes = Files.readAllBytes(path); // 读字节
Files.writeString(path, "hello", StandardOpenOption.CREATE); // Java 11+
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());
}
}
// 递归遍历(Java 8+)
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;

// ========== 1. 基础用法 ==========

// 覆盖写入(文件不存在则创建)
try (FileOutputStream fos = new FileOutputStream("D:/data/out.bin")) {
fos.write(65); // 写入单个字节(ASCII 'A')
fos.write(new byte[]{66, 67}); // 写入字节数组("BC")
String s = "hello";
fos.write(s.getBytes()); // 字符串转字节写入(默认平台编码)
fos.write(s.getBytes("UTF-8"));// 指定编码
// 注意:write 不保证立即落盘,flush/close 才刷缓冲区
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

// 追加写入(第二个参数 append=true)
try (FileOutputStream fos = new FileOutputStream("D:/data/out.bin", true)) {
fos.write("追加内容".getBytes("UTF-8"));
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

// ========== 2. 跳过 N 字节再写(用于断点续传场景) ==========
try (FileOutputStream fos = new FileOutputStream("D:/data/out.bin")) {
fos.getChannel().position(1024); // 跳到第1024字节位置写
fos.write(new byte[]{1, 2, 3});
// 无需手动 close(),try-with-resources 结束自动关闭(close 内含 flush)
}

// ========== 3. 配合输入流做文件拷贝 ==========
try (FileInputStream fis = new FileInputStream("D:/data/src.bin");
FileOutputStream fos = new FileOutputStream("D:/data/dest.bin")) {
byte[] buf = new byte[8192]; // 8KB 缓冲
int len;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len); // 只写本次读取到的有效字节数
}
}

// 推荐:直接用 try-with-resources(上文 1/2/3 已演示),无需手写 finally

要点:

  • 用 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(字节输入流)

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;

// ========== 1. 基础用法:逐字节 / 缓冲数组读取 ==========

// 单字节读取(返回 0~255,读到末尾返回 -1)—— 大文件极慢,仅适合小文件
try (FileInputStream fis = new FileInputStream("D:/data/in.bin")) {
int b;
while ((b = fis.read()) != -1) {
// b 是本次读到的字节
}
}

// 用缓冲数组批量读取(推荐,性能高)
try (FileInputStream fis = new FileInputStream("D:/data/in.bin")) {
byte[] buf = new byte[8192]; // 8KB 缓冲
int len;
while ((len = fis.read(buf)) != -1) {
// 本次有效字节范围 buf[0 .. len-1];len=-1 表示 EOF
sout(new String(buf, 0,len))
}
}

// ========== 2. 一次读取全部字节(小文件) ==========
// readAllBytes() 是 JDK 9+;大文件慎用,会一次性占满堆内存
try (FileInputStream fis = new FileInputStream("D:/data/in.bin")) {
byte[] all = fis.readAllBytes(); // 整文件读入内存
String text = new String(all, "UTF-8"); // 手动指定编码转字符串
}

// ========== 3. 跳过 / 定位读取(断点续传场景) ==========
try (FileInputStream fis = new FileInputStream("D:/data/in.bin")) {
fis.skip(1024); // 跳过前 1024 字节
fis.getChannel().position(2048L); // 或精确定位到第 2048 字节
byte[] buf = new byte[1024];
int len = fis.read(buf); // 从 2048 字节处开始读
}

// ========== 4. 配合输出流做文件拷贝 ==========
// try-with-resources 同时管理输入/输出两个流,异常时两者都会自动关闭(见上文 FileOutputStream 示例3)

要点:

  • 用 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); // c 是本次读到的字符(已按 UTF-8 解码,中文不乱码)
}
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); // 本次有效字符范围 buf[0 .. len-1]
}
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) { // 每次读一整行,不含换行符
// 处理 line
}
}

3. InputStreamReader(字节流 → 字符流桥梁,可指定编码)

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;

// 针对非 UTF-8(如 GBK)文件,用 InputStreamReader 指定解码编码避免乱码
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("D:/data/in.txt"), "GBK"))) {
String line;
while ((line = br.readLine()) != null) {
// 处理 line
}
}

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;

// ========== 1. 基础拷贝(char[] 缓冲,边读边写,保留原换行符) ==========
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]; // 8KB 字符缓冲;可据文件大小调整
int len;
while ((len = br.read(buf)) != -1) { // 读一块字符,len=-1 表示读完
bw.write(buf, 0, len); // 只写本次读到的有效字符(0..len-1)
}
// try-with-resources 自动关闭两个流(close 内含 flush)
}

// ========== 2. 按行拷贝(readLine + newLine,换行符统一为平台默认) ==========
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;

// 第二个参数 true 表示追加;不填或 false 则覆盖
try (FileWriter fw = new FileWriter("D:/data/out.txt", StandardCharsets.UTF_8, false)) {
fw.write("你好,世界");
fw.write("\n");
fw.append("追加内容"); // append 与 write 类似,可链式调用
fw.flush(); // 写完建议 flush(try-with-resources 关闭时也会自动 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;

// 指定 GBK 编码写出
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

缓冲流(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. BufferedInputStream(缓冲字节输入流)

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;

// 构造:包装基础流;第二个参数可自定义缓冲区大小(字节),默认 8192
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("D:/data/in.bin"), 8192)) {

// ========== 用法1:逐字节读取(内部已缓冲,比裸流 read() 快得多) ==========
int b;
while ((b = bis.read()) != -1) { // 先从缓冲区取,缓冲区空了才读一次磁盘
// 处理字节 b
}

// ========== 用法2:批量读取(推荐) ==========
byte[] buf = new byte[8192];
int len;
while ((len = bis.read(buf)) != -1) {
// 有效数据范围 buf[0 .. len-1]
}

// ========== 用法3:mark / reset(缓冲流独有,裸流不支持) ==========
bis.mark(1024); // 在当前位置打标记,参数为标记后最多可回退的字节数
int first = bis.read();
bis.reset(); // 回到 mark 的位置,first2 == first
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;

// 第二个参数可自定义缓冲区大小,默认 8192
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("D:/data/out.bin"))) {
bos.write("hello".getBytes("UTF-8")); // 先写入内存缓冲区,攒满才落盘
bos.write(65); // 写单个字节
bos.flush(); // ★ 缓冲区未满时必须手动 flush,否则数据滞留内存
// try-with-resources 关闭时会自动 flush 再 close
}

// 常见坑:不套缓冲流且忘记 flush / 提前 close 输出流 → 文件内容不完整

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))) {

// ========== 用法1:readLine 按行读(最常用),返回 null 表示 EOF ==========
String line;
while ((line = br.readLine()) != null) {
// line 不含换行符 \n
}

// ========== 用法2:批量读字符 ==========
char[] buf = new char[8192];
int len;
while ((len = br.read(buf)) != -1) {
// 有效字符范围 buf[0 .. len-1]
}

// ========== 用法3:JDK 8+ 按行转 Stream(配合 lambda 最简洁) ==========
// br.lines().filter(l -> !l.isEmpty()).forEach(System.out::println);
}

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(); // ★ 写平台无关换行符(Windows=\r\n,Linux=\n),别手写 "\n"
bw.write("第二行");
bw.append("可链式追加"); // 返回 Writer 本身,可链式
bw.flush(); // 关闭前手动 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); // 只写有效字节 0..len-1
}
// close 时自动 flush;也可显式 bos.flush() 再收尾
}

要点:

  • 缓冲流是装饰器,包装基础流使用,本身不直接连文件(new BufferedReader(path) 这种写法不存在)
  • BufferedOutputStream / BufferedWriter **写完必须 flush()**(或依赖 close 自动 flush),否则缓冲区数据不落盘
  • BufferedReader.readLine() / BufferedWriter.newLine() 是按行处理文本的黄金组合
  • mark() / reset() 只有缓冲流(及少数流)支持,裸 FileInputStream 调用会抛 IOException
  • 只需关闭最外层的缓冲流即可,其 close() 会级联关闭内部被包装的基础流
  • 字节缓冲流用于二进制文件;字符缓冲流用于文本文件,别混用

文件拷贝:边读边写(FileInputStream + FileOutputStream)

核心思路:用输入流 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;

// ========== 1. 基础拷贝(边读边写,固定缓冲) ==========
try (FileInputStream fis = new FileInputStream("D:/data/src.bin");
FileOutputStream fos = new FileOutputStream("D:/data/dest.bin")) {
byte[] buf = new byte[8192]; // 8KB 缓冲;可据文件大小调整为 16KB/32KB
int len;
while ((len = fis.read(buf)) != -1) { // 读一块,len=-1 表示读完
fos.write(buf, 0, len); // 只写本次读到的有效字节(0..len-1)
}
// try-with-resources 自动关闭两个流(close 内含 flush)
}

// ========== 2. 拷贝并统计字节数 / 显示进度 ==========
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;
// System.out.println("已拷贝 " + total + " 字节");
}
}

// ========== 3. 拷贝时跳过前 N 字节(如去掉文件头) ==========
try (FileInputStream fis = new FileInputStream("D:/data/src.bin");
FileOutputStream fos = new FileOutputStream("D:/data/dest.bin")) {
fis.skip(16); // 跳过源文件前 16 字节
byte[] buf = new byte[8192];
int len;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
}

// ========== 4. 拷贝失败先删半成品(避免产生损坏文件) ==========
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,一行搞定)

对象流(ObjectInputStream / ObjectOutputStream)

对象流负责序列化(对象 → 字节)与反序列化(字节 → 对象),可把 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;

// ========== 0. 待序列化实体(必须实现 Serializable) ==========
// ★ 强烈建议显式声明 serialVersionUID,否则类结构变动后旧数据反序列化会抛 InvalidClassException
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // transient:序列化时跳过此字段(如密码等敏感信息)

public User(String name, int age, String password) {
this.name = name; this.age = age; this.password = password;
}
// getter / toString 省略
}

// ========== 1. 序列化:对象写进文件 ==========
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("D:/data/user.dat")))) {
User u = new User("张三", 18, "secret");
oos.writeObject(u); // 真正写出对象(含类的结构信息,password 因 transient 不写)
oos.flush();
}

// ========== 2. 反序列化:从文件读回对象 ==========
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("D:/data/user.dat")))) {
User u = (User) ois.readObject(); // ★ 返回 Object,需强转;失败抛 ClassNotFoundException
System.out.println(u.getName() + " / " + u.getAge());
// u.getPassword() == null(transient 字段反序列化后为默认值)
}

// ========== 3. 一次读写多个对象(用集合打包最稳妥,避免 EOFException 判断) ==========
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); // 直接写整个 List
}
// 读集合
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 / OutputStreamWriter)

转换流是字节流 ↔ 字符流之间唯一的桥梁: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.*;

// ========== 一、JDK 8 写法:手写 InputStreamReader / OutputStreamWriter ==========
// 读:字节流 → 转换流(指定编码) → 缓冲流(按行读)
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();
}

// GBK 文件(非 UTF-8 必须手动指定,否则平台默认编码易乱码)
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("D:/data/gbk.txt"), "GBK"))) {
// ...
}

// ========== 二、JDK 11 写法:Files 工具类(内部已用转换流,无需手写) ==========
// 读:Files.newBufferedReader 直接返回带编码的 BufferedReader(JDK 8+ 即支持)
try (BufferedReader br = Files.newBufferedReader(
Paths.get("D:/data/in.txt"), StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
// 处理一行
}
}

// 写:Files.newBufferedWriter 直接返回带编码的 BufferedWriter
try (BufferedWriter bw = Files.newBufferedWriter(
Paths.get("D:/data/out.txt"), StandardCharsets.UTF_8)) {
bw.write("你好");
}

// 极简:整文件一次读写(JDK 11+)
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
// ========== PrintWriter(字符打印流,推荐用于文本) ==========
// 自动行刷新:println 后自动 flush(构造第二个参数 autoFlush=true)
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("D:/data/log.txt")), true)) {
pw.println("普通字符串");
pw.println(123); // 自动转字符串,无需 String.valueOf
pw.println(3.14);
pw.println(true);
pw.printf("姓名:%s,年龄:%d%n", "张三", 25); // 格式化输出,%n 是平台无关换行
}

// 指定编码(PrintWriter 不能直接传 Charset,需套转换流)
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("D:/data/utf8.txt"), StandardCharsets.UTF_8)))) {
pw.println("指定 UTF-8 编码写入");
}

// ========== PrintStream(字节打印流) ==========
// System.out 就是 PrintStream
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("这行会写进文件而不是控制台");
}
// 恢复:提前保存 System.setOut(oldOut)

// 吞异常:打印流的所有写方法都不抛 IOException
// 想确认是否出错,用 checkError()
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 路径穿越(解压时校验条目名不能含 ..)见下方解压流

解压流(ZipInputStream / GZIPInputStream)

解压流是输入流的包装器,读取时被自动解压,用法与普通输入流完全一致。

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
// ========== 一、ZipInputStream:解压 .zip,逐个条目遍历 ==========
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{
// new File(new File(path, zipEntry.getName()).getParent()).mkdirs();
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();
}
}


// ========== 二、GZIPInputStream:解压 .gz(无条目概念,直接读) ==========
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());
}
}

// ========== 三、ZipFile:随机访问(按名字直接取,不用顺序遍历) ==========
// 适合"只要压缩包里的某一个文件",比 ZipInputStream 快得多
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); // JDK 9+
}
}
}

要点:

  • 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;

// GET
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(); // 200
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();

// POST(JSON 请求体)
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();

// GET
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());
}

// POST(JSON)
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()) {
// GET
HttpGet get = new HttpGet("https://api.example.com/user/1");
http.execute(get, response -> {
System.out.println(response.getCode());
return null;
});

// POST(JSON)
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 对象(三种)
Class<?> c1 = User.class;
Class<?> c2 = userObj.getClass();
Class<?> c3 = Class.forName("com.example.User"); // 按配置字符串动态加载

// 创建实例:JDK9+ 用构造器,clazz.newInstance() 已废弃
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); // public(含继承)
m.invoke(u, "jerry");
Method pm = c1.getDeclaredMethod("privateInit"); // private/protected 用 getDeclaredXxx
pm.setAccessible(true); // 突破私有限制(受模块系统/安全管理器约束)
Object ret = pm.invoke(u);

// 获取成员变量(Field)
Field[] pubFields = c1.getFields(); // 所有 public 字段(含父类继承来的)
Field[] allFields = c1.getDeclaredFields(); // 本类声明的全部字段(含 private,不含继承)

Field f = c1.getField("name"); // 按名取 public 字段(含继承),找不到抛 NoSuchFieldException
Field df = c1.getDeclaredField("name"); // 按名取本类任意权限字段(常用)
df.setAccessible(true); // private 字段必须开权限才能读写

// 字段元信息
df.getName(); // "name" 字段名
df.getType(); // String.class 声明类型
df.getGenericType(); // 带泛型的声明类型,如 List<String>
df.getModifiers(); // 修饰符位掩码
Modifier.isPrivate(df.getModifiers()); // 是否 private
Modifier.isStatic(df.getModifiers()); // 是否 static
Modifier.isFinal(df.getModifiers()); // 是否 final
df.getAnnotation(Deprecated.class); // 字段上的注解(null=无)

// 取值 / 赋值
String name = (String) df.get(u); // 实例字段:传所属对象, U真实的构造 函数
df.set(u, "jack");
df.set(null, "x"); // static 字段:obj 传 null
int age = df.getInt(u); // 基本类型可用 getInt/getLong/getBoolean... 免拆箱
df.setInt(u, 18);

// 遍历「字段名 = 值」(Bean 转 Map / Excel 导出等通用工具常用)
for (Field fd : c1.getDeclaredFields()) {
fd.setAccessible(true);
System.out.println(fd.getName() + " = " + fd.get(u));
}

// 父类字段:getDeclaredField 不含继承,需沿 getSuperclass() 逐层向上找
Field parentF = c1.getSuperclass().getDeclaredField("id");

// 静态方法/静态字段:invoke、get 的第一个参数传 null
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
// 生成随机UUID
String uuid = UUID.randomUUID().toString(); // 带横线 "550e8400-e29b-41d4-a716-446655440000"
String uuid32 = uuid.replace("-", ""); // 去横线 32位

// 基于名称的UUID(相同输入总是相同输出)
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
// 读取 classpath 下的 config.properties
// 推荐方式
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(); // 是否有下一个token

// 从文件读取
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(); // JVM最大可用内存
long totalMem = rt.totalMemory(); // 当前已分配内存
long freeMem = rt.freeMemory(); // 空闲内存
int cpus = rt.availableProcessors(); // CPU核心数

// 执行外部命令
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(); // 等待命令执行完毕

// JDK9+ ProcessBuilder(推荐)
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
// 方式1:实体类实现 Comparable(自然排序)
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); // 直接排序

// 方式2:外部 Comparator(灵活,不侵入实体类)
// 按年龄升序
userList.sort(Comparator.comparingInt(User::getAge));
// 按年龄降序
userList.sort(Comparator.comparingInt(User::getAge).reversed());
// 多字段排序:先按部门,再按年龄
userList.sort(Comparator.comparing(User::getDept).thenComparingInt(User::getAge));
// null 安全排序
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 对象
Class<?> clazz = String.class;
Class<?> clazz2 = "hello".getClass();
Class<?> clazz3 = Class.forName("java.lang.String");

// 类型判断
obj instanceof String // 运行时判断
String.class.isInstance(obj) // 等价于 instanceof
String.class.isAssignableFrom(obj.getClass()) // 可赋值判断

// 获取类信息
clazz.getName(); // "java.lang.String"
clazz.getSimpleName(); // "String"
clazz.getSuperclass(); // Object.class
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(先进先出 FIFO)
Queue<String> queue = new LinkedList<>();
queue.offer("A"); // 入队(推荐,满时返回false)
queue.add("B"); // 入队(满时抛异常)
queue.peek(); // 查看队首(不移除,空返回null)
queue.poll(); // 出队(空返回null)
queue.remove(); // 出队(空抛异常)

// Deque 当栈用(后进先出 LIFO)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 压栈
stack.push(2);
stack.peek(); // 查看栈顶 → 2
stack.pop(); // 弹栈 → 2

// PriorityQueue(优先队列/小顶堆)
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 自然排序(小顶堆)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); // 大顶堆
pq.offer(5); pq.offer(1); pq.offer(3);
pq.poll(); // 1(最小值先出)

场景: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); // 设置第0位为1
bs.set(5); // 设置第5位为1
bs.set(10, 20); // 设置[10,20)为1
bs.get(5); // true
bs.clear(5); // 清除第5位
bs.cardinality(); // 为1的位数
bs.isEmpty(); // 是否全为0

// 位运算
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;
}

// getter/setter...
}

泛型方法

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
// ? extends T — 上界通配符(只读,生产者)
public void printList(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}

// ? super T — 下界通配符(只写,消费者)
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}

// PECS 原则:Producer-Extends, Consumer-Super

类型擦除

1
2
3
4
5
6
7
// 编译后泛型信息被擦除,以下两个方法签名冲突(编译报错)
// public void process(List<String> list) {}
// public void process(List<Integer> list) {} // 擦除后签名相同

// 运行时无法获取泛型的实际类型参数
List<String> list = new ArrayList<>();
System.out.println(list.getClass()); // class java.util.ArrayList(没有String信息)

四、异常处理(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) {
// 多个catch从小到大
e.printStackTrace();
} finally {
// 无论是否异常都会执行(释放资源)
System.out.println("finally块执行");
}

try-with-resources(JDK7+)

1
2
3
4
5
6
7
8
9
10
11
// 实现了 AutoCloseable 的资源会自动关闭
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();
}
// 不需要手动 finally { fis.close(); br.close(); }

自定义异常

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);

// BufferedReader 逐行读取大文件
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(旧API)
File file = new File("/path/to/file.txt");
file.exists();
file.isFile();
file.isDirectory();
file.length(); // 文件大小(字节)
file.getName(); // 文件名
file.getParent(); // 父目录
file.listFiles(); // 列出子文件

// Path + Files(JDK7+ 推荐)
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
// Java 原生序列化(不推荐用于网络传输,推荐用 JSON)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // transient 字段不参与序列化
}

// JSON 序列化(Jackson — Spring Boot 默认)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user); // 对象 → JSON
User user = mapper.readValue(json, User.class); // JSON → 对象
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
// 方式1:继承Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " running");
}
}
new MyThread().start();

// 方式2:实现 Runnable 接口(推荐,任务与线程解耦)
// 2.1 定义任务类:实现 Runnable 接口,重写 run()(run() 是"任务体",不是线程启动入口)
class DownloadTask implements Runnable {
private final String url;

public DownloadTask(String url) {
this.url = url; // 构造参数即"任务数据"
}

@Override
public void run() {
// 线程名通过 Thread.currentThread().getName() 获取
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);
}
}



// 2.4 同一个 Runnable 实例可被多个线程共享(任务无可变共享状态,或自行保证线程安全)
Runnable shared = new DownloadTask("shared.zip");
new Thread(shared, "worker-2").start();
new Thread(shared, "worker-3").start();


// 方式3:Callable + FutureTask(有返回值、可抛异常)
// 3.1 实现 Callable 接口:相比 Runnable,有返回值且能抛受检异常
class SumTask implements Callable<Integer> {
private final int n;

public SumTask(int n) {
this.n = n;
}

@Override
public Integer call() throws Exception { // ★ 可声明 throws,异常能被调用方感知
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
Thread.sleep(10); // 模拟耗时计算
}
return sum; // ★ 返回值类型 = 泛型实参 Integer
}
}

// 3.2 用 FutureTask 包装:它同时实现 Runnable + Future,既能被线程执行、又能取结果
FutureTask<Integer> task = new FutureTask<>(new SumTask(10));
new Thread(task, "calc-thread").start();

// 3.3 获取结果:get() 会阻塞当前线程,直到任务执行完
Integer result = task.get(); // 阻塞等待
// Integer result = task.get(2, TimeUnit.SECONDS); // ★ 推荐:带超时,超时抛 TimeoutException
System.out.println("结果:" + result); // 55

// 3.4 取消与状态判断(Future 接口提供;一般用在任务"还没跑完"的时候)
task.isDone(); // 是否已结束(正常完成 / 抛异常 / 被取消,都算 true)
task.isCancelled(); // 是否被取消
task.cancel(true); // 取消任务(true = 尝试中断正在执行的线程);已完成的任务 cancel 无效

// 3.5 任务抛异常时:异常被 FutureTask 捕获,不会在子线程直接抛出,而是 get() 时包装抛出
try {
task.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // ★ 原始异常:call() 里真正抛出的那个
} catch (CancellationException e) {
// 任务已 cancel,此时再 get() 会抛此异常
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 等待过程中被中断,恢复中断标记
}

// 3.6 实际项目写法:提交给线程池,直接拿 Future(submit 内部就是 new FutureTask)
// 线程池的创建见下一节「线程池」;生产环境任务都走池子,不要自己 new Thread()
Future<Integer> future = pool.submit(new SumTask(10)); // submit(Callable) 直接返回 Future
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()); // NEW(还没 start)
t.start();
System.out.println(t.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING(卡在 sleep(1000))
t.join();
System.out.println(t.getState()); // TERMINATED

状态流转

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
// 推荐手动创建(阿里规约禁止 Executors.newXxx)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列 new ArrayBlockingQueue<>(3)
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.*;

// 1. newFixedThreadPool:固定线程数(最多 5 个线程并发,超出的任务在队列里排队)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 案例:批量提交 10 个任务,统一收集结果求和
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
final int taskId = i; // Lambda 里必须用 final/等效 final
futures.add(fixedPool.submit(() -> { // submit(Callable):有返回值
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();
}

// 2. newCachedThreadPool:线程数无上限,来了任务就创建新线程(空闲 60s 回收)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 案例:并发跑一批互不相干的短任务,不关心返回值
for (String url : urls) {
cachedPool.execute(() -> fetch(url)); // execute(Runnable):无返回值
}
// 需要结果时用 submit,异常同样在 get() 时才抛 ExecutionException
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
// newFixedThreadPool(5) 的安全等价写法
new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列,替代无界
namedFactory,
new ThreadPoolExecutor.AbortPolicy());

// newCachedThreadPool() 的安全等价写法
new ThreadPoolExecutor(0, 50, // 限制最大线程数,替代 Integer.MAX_VALUE
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();
}
}

/** 手动使用 Lock(ReentrantLock) */
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;
// 静态锁:多个 Order 实例共享同一把锁
private static final Lock lock = new ReentrantLock();

public void pay() {
while (true) {
// 1. 手动加锁
lock.lock();
try {
// 2. 临界区
if (sum >= 100) {
break; // 注意:break 前会自动执行 finally 的 unlock
}
sum++;
System.out.println(Thread.currentThread().getName()
+ "售出1张票,已出售" + sum + ",剩余" + (100 - sum) + "张票");
} finally {
// 3. 必须在 finally 中手动释放锁,防止异常导致死锁
lock.unlock();
}
}
}

@Override
public void run() {
pay();
}
}

/** tryLock:尝试拿锁,拿不到立刻返回,避免死等 */
public void payTryLock() {
while (true) {
if (lock.tryLock()) { // 无参:立即返回 boolean
try {
if (sum >= 100) break;
sum++;
System.out.println(Thread.currentThread().getName() + "售出1张票");
} finally {
lock.unlock();
}
} else {
// 拿不到锁,去做别的事,稍后再试
System.out.println(Thread.currentThread().getName() + "未拿到锁,稍后重试");
}
}
}

// 带超时的 tryLock:最多等 1 秒
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
}

/** Condition:Lock 的等待/通知机制,替代 wait/notify */
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(); // 等价于 wait()
}
sum++;
notFull.signalAll(); // 等价于 notifyAll()
} 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
// volatile:保证可见性和有序性,不保证原子性
private volatile boolean running = true;

// 原子类:保证原子性(CAS实现)
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++count
count.getAndIncrement(); // count++
count.addAndGet(5); // count += 5
count.compareAndSet(5, 10); // CAS:期望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:等待N个线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
pool.execute(() -> {
try { doWork(); } finally { latch.countDown(); }
});
}
latch.await(); // 主线程等待3个任务完成

// CyclicBarrier:N个线程互相等待到齐后一起执行
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("全部到齐"));
for (int i = 0; i < 3; i++) {
pool.execute(() -> {
prepare();
barrier.await(); // 等待其他线程
execute();
});
}

// Semaphore:控制并发数(限流)
Semaphore semaphore = new Semaphore(5); // 最多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
  • 本文标题:java开发文档
  • 本文作者:邵预鸿
  • 创建时间:2026-06-11 11:55:22
  • 本文链接:/images/logo.jpg2026/06/11/java开发文档/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!