【阶段 0】Java 前置基础:从语法到生产级代码

170次阅读
没有评论

阶段 0 · 纯 Java · JDK 17 本阶段是后续 11 个 Spring Boot
阶段的「地基」:不涉及任何 Spring
概念,只解决一个问题——把一个有编程基础、但没写过生产级 Java
的人,训练成能写出健壮、可维护、可运行 Java 代码的后端预备选手。


1. 导语

你可能会问:都要学 Spring Boot 了,为什么还要回头补 Java?

因为 Spring Boot 本身就是一个 Java
框架。框架替你做的事越多,你对底层「为什么会这样、坑在哪里」的判断就越重要。真实生产环境的排查,往往不是「报错了找不到方法」这种明面上的问题,而是下面这类隐性问题:

  • 一段 HashMap 遍历,线上偶发
    ConcurrentModificationException,本地却怎么都复现不出来;
  • 一个实体类只重写了 equals 没重写
    hashCode,导致 Set 去重和 Map
    查找「看运气」失效;
  • Executors.newFixedThreadPool()
    创建线程池,大促流量一上来直接 OOM;
  • ThreadLocal 用了不
    remove,线程池复用线程后用户 A 读到了用户 B 的
    traceId。

这些都不是「语法不会」,而是「没有建立生产级编码的肌肉记忆」。本阶段的目标就是把这块地基打牢。

学完本阶段,你能做到:

  1. 独立写出可运行、有校验、有异常处理、有日志、有注释的 Java
    代码;
  2. 讲清 HashMap 的底层原理,能根据场景正确选型集合;
  3. 写出泛型工具类、自定义注解,并用反射看懂框架级代码;
  4. StreamOptional
    写出简洁且不踩坑的数据处理代码;
  5. 手动配置生产级线程池,理解
    volatile/synchronized/CAS
    的边界;
  6. 用 Maven
    管理一个多模块项目,并串起一个完整的「用户管理」小项目。

本阶段与后续阶段的关系:它不依赖任何前置阶段(有任意编程语言经验即可);它产出的
ErrorCodeBizException 等公共类,会被后续
Spring Boot
阶段逐字复用。所以这一篇请务必逐行敲一遍,而不是只读。


2. 学习目标与前置要求

学完你能…

  • 能搭环境:独立安装/切换 JDK 17,配置
    JAVA_HOME,用 IDEA 断点调试一个程序。
  • 能选集合:讲清
    ArrayList/LinkedList/HashMap/HashSet
    的底层与选型,正确重写 equals/hashCode
  • 能写框架级代码:用泛型 PECS
    写工具方法,自定义注解并用反射读取,理解动态代理。
  • 能写简洁代码:用 Stream
    三阶段完成过滤、分组、转 Map、统计,用 Optional
    正确表达「可能为空」。
  • 能建异常体系:定义 ErrorCode +
    BizException,配置 SLF4J + Logback 日志。
  • 能配线程池:手写 ThreadPoolExecutor
    七参数与拒绝策略,用 ThreadLocal 传 traceId 且不泄漏。
  • 能用
    Maven
    :管理依赖冲突、多模块项目,理解私服的作用。
  • 能写规范代码:遵循阿里 Java 开发手册与 Clean
    Code,串起「用户管理」小项目。

前置要求

  • 依赖阶段:无。有任意一门编程语言(Python / JS / Go
    / C 等)的使用经验即可。
  • 建议具备:基本数据类型、循环、函数、面向对象的概念性理解。若完全零基础,建议先看任意一本
    Java 入门书(如《Head First Java》)的前 6 章,再回到本篇。
  • 工具:一台能联网的电脑,安装 JDK 17、IntelliJ
    IDEA(社区版即可)、Maven 3.8+。

3. 环境准备

本篇所有代码统一基于 JDK 17(LTS 版本,Spring Boot
3.x 要求的最低版本)。下文给出 Linux/macOS/Windows
三平台的安装要点,重点讲清 JAVA_HOME
与「版本切换」这两个新手最容易卡住的点。

3.1 安装 JDK 17

推荐使用 OpenJDK 的发行版 Eclipse Temurin(原
AdoptOpenJDK),免费且生产可用。

  • Linux(Ubuntu/Debian)
# 安装 Temurin 仓库后执行
sudo apt-get update
sudo apt-get install -y temurin-17-jdk
  • macOS(Homebrew)
brew install --cask temurin@17
  • Windows:到 Adoptium
    官网
    下载 .msi 安装包,一路下一步,安装时勾选「Set
    JAVA_HOME variable」。

3.2 配置 JAVA_HOME 与环境变量

JAVA_HOME 是很多工具(Maven、Gradle、IDEA、Tomcat)寻找
JDK 的唯一依据,必须指向 JDK
安装根目录(不是 bin 目录)。

  • Linux/macOS,编辑 ~/.bashrc(或
    ~/.zshrc):
# 替换成你实际的安装路径(可用 `readlink -f $(which java)` 查到)
export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
source ~/.bashrc   # 让配置立即生效
  • Windows:系统环境变量中新建
    JAVA_HOME,值为
    C:Program FilesEclipse Adoptiumjdk-17.0.x.y-hotspot,再把
    %JAVA_HOME%bin 追加到 Path

3.3 验证与版本切换

java -version
# 期望输出类似:
# openjdk version "17.0.12" 2024-07-16
# OpenJDK Runtime Environment Temurin-17.0.12+7 (build 17.0.12+7)
# OpenJDK 64-Bit Server VM Temurin-17.0.12+7 (build 17.0.12+7)

javac -version   # javac 17.0.12

版本切换:如果你机器上装了多个
JDK,切换的本质就是
JAVA_HOME
,而不是「卸载重装」。常用的三种做法:

  1. 手动改环境变量(最朴素);
  2. 用 SDKMAN(Linux/macOS):sdk install java 17.0.12-tem
    sdk use java 17.0.12-tem
  3. Windows 用 setx JAVA_HOME "..." 后重开终端。

关键点:java -version 显示的是 PATH
里第一个 java 的版本,javac -version
同理。判断「我到底在哪个 JDK 下编译」就看 JAVA_HOME,别只看
java -version

3.4 安装 IDEA 与 Maven

  • IntelliJ IDEA Community:到 JetBrains
    官网下载社区版即可,本篇用到的调试、重构功能社区版全都有。
  • Maven:Linux/macOS 用
    brew install maven
    apt-get install maven;Windows 下载 zip 解压后配
    MAVEN_HOMEPath。验证:
mvn -version
# Apache Maven 3.9.x ...

第 1~6 章的绝大多数示例用 JDK 自带的
javac/java 即可运行,不依赖
Maven
;只有第 5 章日志、第 7 章及实战项目用到
Maven。这样安排是为了让你先用最小的工具负担把语言本身学扎实。


4. 正文章节

第 1 章 环境搭建与工具链

1.1 第一个程序:HelloWorld

新建文件 HelloWorld.java,内容如下:

// HelloWorld.java —— JDK 11+ 支持直接 `java HelloWorld.java` 单文件运行,无需先 javac
public class HelloWorld {
    public static void main(String[] args) {
        // println 是 System.out 的静态调用;生产代码禁止用 System.out 打日志,见第 5 章
        System.out.println("Hello, Java 17!");
    }
}

两种运行方式:

# 方式一:传统两步(编译 + 运行)
javac HelloWorld.java      # 生成 HelloWorld.class 字节码
java HelloWorld            # 运行字节码,输出 Hello, Java 17!

# 方式二:单文件源码启动(JDK 11+ 特性,适合快速验证)
java HelloWorld.java

javac.java 编译成
.class(字节码),java 通过 JVM
解释/即时编译执行字节码。这是
Java「一次编译,到处运行」的基石——.class
文件与操作系统无关,只要有对应版本的 JVM 就能跑。

1.2 生产级习惯:从第一行就做对

虽然只是 HelloWorld,但两个生产级习惯要从现在建立:

  1. 类名与文件名一致且用
    UpperCamelCase
    HelloWorld
    HelloWorld.java)。这是 Java 编译器的硬性要求(对
    public 类而言),也是阿里手册的命名规范。
  2. 一个 .java 文件只有一个 public
    顶层类
    。多个 public 类会直接编译报错。

1.3 IDEA 高效使用

IDEA 是 Java 后端的事实标准
IDE。下面这些快捷键会伴随你整个职业生涯,第 1 天就练成肌肉记忆:

快捷键(Windows/Linux) macOS 作用
psvm + Tab 同左 生成 public static void main
sout + Tab 同左 生成 System.out.println()
Alt + Enter Option + Enter 万能修复:导包、补异常、抽变量
Ctrl + Shift + F10 Ctrl + Shift + R 运行当前类
Ctrl + Alt + L Option + Cmd + L 格式化代码
F7 / F8 / F9 同左 调试:单步进入 / 单步跳过 / 跳到下一个断点
Ctrl + Shift + A Cmd + Shift + A 搜索任意动作(迷路时用它)

1.4
断点调试演示(生产排查的起点)

生产环境排查问题的核心能力,是在本地复现 +
断点观察变量
。下面用一段有 bug 的代码演示完整调试流程:

// DebugDemo.java —— 一个刻意写错的累加逻辑,用来演示断点
public class DebugDemo {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5};
        int sum = sumEven(nums);   // 期望只累加偶数,结果却不对
        System.out.println("偶数之和 = " + sum);
    }

    // 注意:循环里 i++ 与 i+=2 的边界差异,故意留 bug
    private static int sumEven(int[] arr) {
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];          // 这里没判断奇偶,是 bug
        }
        return sum;
    }
}

调试步骤:

  1. sum += arr[i];
    这一行左侧单击,打上断点(红点);
  2. 右键 Debug 'DebugDemo' 启动(而非 Run);
  3. 程序停在断点处,观察下方 Variables 面板中
    iarr[i]sum 的实时值;
  4. F8 逐行执行,看 sum
    每次加的是奇数还是偶数——你会立刻发现 i
    走到奇数下标时也在累加;
  5. 修复:sum += arr[i]; 改为
    if (arr[i] % 2 == 0) { sum += arr[i]; }
  6. 再运行,输出 偶数之和 = 6(2 + 4)。

关键点:「会打断点、会看变量、会单步执行」是后端工程师的基本功。遇到「本地不报错、线上报错」,第一反应永远是「在可疑行打断点,看变量值到底变成了什么」,而不是对着代码空想。

1.5 JDK / JRE /
JVM:三个词别再混了

这是面试和日常沟通的高频概念,必须分清:

  • JVM(Java Virtual
    Machine)
    :执行字节码的虚拟机,负责类加载、内存管理、垃圾回收(GC)、JIT
    即时编译。
  • JRE(Java Runtime Environment):JVM +
    核心类库(基础类),只能运行 Java 程序,不能编译。
  • JDK(Java Development Kit):JRE +
    开发工具(javac 编译器、jdb
    调试器、javadoc 文档生成器、jconsole
    监控等)。

关系:JDK ⊃ JRE ⊃ JVM。开发机装
JDK;生产机器最小化部署往往只装 JRE(容器化时代通常直接装完整
JDK,方便线上 jstack/jmap 排查)。

Java「一次编译、到处运行」的本质:javac
.java
编译成平台无关的字节码.class),不同操作系统上的
JVM
各自解释/即时编译执行这份字节码。所以「到处运行」靠的是各平台都有对应的
JVM
,而不是字节码本身能直接跑。

1.6 IDEA 创建项目与目录结构

IDEA 新建 Maven
项目(File → New → Project → Maven)会生成标准目录结构:

demo/
├── pom.xml                     # Maven 构建配置(第 7 章详解)
└── src/
    ├── main/
    │   ├── java/               # 业务代码(包名对应目录层级)
    │   └── resources/          # 配置文件(logback.xml、application.yml 等)
    └── test/
        └── java/               # 单元测试代码(与 main 同包结构)

关键点:src/main/java
是代码的家,src/main/resources
是配置的家,src/test/java 是测试的家。这是 Maven
约定优于配置——目录摆对,Maven
自动识别,无需额外配置。

1.7 调试进阶:不止 F8

调试的核心是「控制程序节奏 + 观察状态」。三个进阶技巧:

  1. 条件断点:在断点上右键,输入条件(如
    i == 100),只有条件成立才停下,避免大循环里一步步按到手抽筋;
  2. Evaluate ExpressionAlt + F8 /
    Option + F8):断点处临时执行一段表达式,查看或修改变量;
  3. Watch:把某个变量加入监视,全程追踪它的变化,不用每次在
    Variables 面板里翻找。

1.8 环境排错三招

现象 原因 解决
java 能用,javac
报「不是内部或外部命令」
JAVA_HOME 没配,或 Path 没加
%JAVA_HOME%bin
检查两个环境变量是否都配好
java -versionjavac -version
版本不一致
两个命令在 Path 里找到了不同 JDK 统一 JAVA_HOME,把 %JAVA_HOME%bin
Path 最前
改了环境变量不生效 终端缓存了旧环境 Windows 重开终端;Linux/macOS 执行 source ~/.bashrc
或重开 shell

1.9
classpath:类从哪里来(入门必懂)

java HelloWorld 能跑,是因为 JVM 知道去哪里找
HelloWorld.class。JVM 找类的搜索路径就叫
classpath,默认是当前目录(.)。

当你的类依赖别的类(比如第 5 章的
ErrorCodeBizException),运行时就要让 JVM
能找到它们:

# -d 指定 .class 输出目录;-cp 指定运行时 classpath
javac -d out ErrorCode.java BizException.java BizDemo.java
java -cp out BizDemo

关键点:编译用 javac -d out.class
输出到 out 目录,运行时用 java -cp out 告诉
JVM 去 out 里找类。第 3 章反射示例、第 6
章线程池示例都走这个套路。Maven(第 7 章)本质上就是在替你管理
classpath——自动下载依赖 jar 并拼进 classpath。

本章小结

JDK 17 安装、JAVA_HOME 配置、IDEA
快捷键与断点调试,是后续所有章节的操作基础;从第一行代码起就遵守命名规范、用调试器替代
print 排查问题。

第 2 章 面向对象与集合框架

后端 90%
的代码在操作集合。本章前半讲面向对象的四大支柱,后半深入集合框架——尤其是
HashMap 的原理与 equals/hashCode
契约,这是面试必考、生产必踩的点。

2.1
面向对象四支柱(快速过一遍)

// OopDemo.java —— 类 / 继承 / 多态 / 抽象类 / 接口 的最小可用示例
public class OopDemo {

    // 抽象类:不能 new,抽取公共状态与模板方法
    abstract static class Animal {
        protected String name;
        Animal(String name) { this.name = name; }
        abstract String sound();          // 抽象方法,子类必须实现
        String introduce() {              // 模板方法:复用不变逻辑
            return name + " 叫:" + sound();
        }
    }

    // 接口:定义能力契约,可多实现(Java 类是单继承、接口多实现)
    interface Pet {
        void play();                       // 接口方法默认 public abstract
    }

    // 子类:继承抽象类 + 实现接口
    static class Dog extends Animal implements Pet {
        Dog(String name) { super(name); }
        @Override String sound() { return "汪汪"; }
        @Override public void play() { System.out.println(name + " 在接飞盘"); }
    }

    static class Cat extends Animal implements Pet {
        Cat(String name) { super(name); }
        @Override String sound() { return "喵喵"; }
        @Override public void play() { System.out.println(name + " 在玩毛线球"); }
    }

    public static void main(String[] args) {
        // 多态:父类引用指向子类对象,运行时才决定调用哪个实现
        Animal a1 = new Dog("旺财");
        Animal a2 = new Cat("咪咪");
        System.out.println(a1.introduce());   // 旺财 叫:汪汪
        System.out.println(a2.introduce());   // 咪咪 叫:喵喵
        ((Pet) a1).play();                     // 转型到接口,调用 play
    }
}

四个概念一句话定位:

  • 封装:用 private
    隐藏内部状态,只暴露必要方法,防止外部随意破坏对象状态。
  • 继承extends 复用父类代码;Java
    单继承(一个类只能有一个父类),用接口弥补「多继承」需求。
  • 多态:父类引用指向子类对象,方法调用在运行时动态绑定(动态分派)。这是
    Spring 里「面向接口编程」的语言基础。
  • 抽象abstract 类/方法与
    interface
    都是「只定契约、不写实现」,把「做什么」和「怎么做」分离。

2.2 集合框架总览与选型

Java 集合分两大接口族:Collection(单列:List / Set /
Queue)和 Map(双列:键值对)。生产选型一句话决策表:

集合 底层结构 特点 生产选型场景
ArrayList 动态数组 按下标查快 O(1),中间插入慢 O(n) 默认 List 选择,读多写少
LinkedList 双向链表 头尾插入删除快 O(1),按下标查慢 O(n) 需要频繁头插/当队列(一般用 ArrayDeque 替代)
HashMap 数组+链表+红黑树 平均 O(1) 查 默认 Map 选择
LinkedHashMap 链表 + 哈希 保持插入顺序 需要有序遍历 / 做 LRU 缓存
TreeMap 红黑树 按 key 排序 需要 key 有序(如按字典序遍历)
HashSet 内部就是 HashMap 去重 去重、判断存在性
ConcurrentHashMap CAS + 分段锁(JDK8 后) 并发安全且高效 并发场景替代 HashTable

一句话:能确定用 ArrayList 就用
ArrayList,不确定就用
ArrayList
LinkedList
在生产里几乎只出现在「实现队列」的场合,而队列首选
ArrayDeque

2.3 HashMap
底层原理(本章重点)

HashMap 是面试必考。你需要能不看笔记讲出下面这条 put
流程:

  1. 定位桶:对 key 调
    hashCode(),再经扰动函数 hash(key)(高 16
    位与低 16 位异或,减少哈希碰撞),最后 (n - 1) & hash
    定位到数组下标(n 是数组长度)。
  2. 插入:该桶为空则直接放入;有元素则用
    equals 逐个比对(先比 ==,再比
    equals),key 相同则覆盖 value,否则以链表尾插。
  3. 树化:链表长度 ≥ 8 且数组长度 ≥ 64
    时,链表转红黑树,把最坏 O(n) 降到 O(log n)。
  4. 扩容:元素个数超过
    容量 × 负载因子(0.75) 时,数组扩容为 2
    ,所有元素重新散列(rehash)。

三个高频追问的答案:

  • 为什么容量是 2 的幂? 因为
    (n - 1) & hash 等价于
    hash % n,但位运算快得多;且 n - 1 全为
    1,能让 hash 的每一位都参与运算,分布更均匀。
  • 什么时候树化? 链表长度 ≥ 8
    数组长度 ≥ 64
    两个条件同时满足(数组太小时会先扩容而不是树化,因为扩容性价比更高)。
  • 为什么 JDK8 从「头插」改成「尾插」? 头插在并发
    resize 时可能形成环形链表,导致 get
    死循环;尾插规避了这个问题。但这不代表 HashMap
    线程安全
    ——并发写仍要 ConcurrentHashMap

2.4 equals 与 hashCode
契约(生产必踩)

契约只有三条,但每一条都有血泪教训:

  1. equals 相等的两个对象,hashCode
    必须相等;
  2. hashCode 相等,equals
    不一定相等(哈希碰撞);
  3. 重写 equals 必须同时重写
    hashCode,反之亦然。

为什么必须同时重写?因为 HashSet/HashMap
先按 hashCode 找桶,再用 equals
比对
。如果你只重写了 equals 不重写
hashCode,两个业务上相等的对象会因 hashCode
不同而落到不同桶,Set 去重、Map
查找直接「看运气」失效。

// User.java —— 生产级:正确重写 equals / hashCode(用 Objects 工具类,规避 null 判断)
import java.util.Objects;

public class User {
    private Long id;          // 数据库主键,业务上唯一标识
    private String username;
    private Integer age;

    public User(Long id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    // equals:先比引用,再判类型,最后逐字段比对(关键字段是业务主键 id)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                 // 同一引用,直接相等
        if (!(o instanceof User)) return false;      // 类型不符,直接不等(instanceof 天然防空)
        User user = (User) o;
        return Objects.equals(id, user.id);          // Objects.equals 内部处理 null,避免 NPE
    }

    // hashCode:与 equals 用到的字段保持一致(都用 id)
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    public Long getId() { return id; }
    public String getUsername() { return username; }
    public Integer getAge() { return age; }

    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', age=" + age + '}';
    }
}

关键点提示:

  • Objects.equals 而不是
    a.equals(b)
    :前者内部先判
    a == null,避免 NullPointerException
  • equals 里用 instanceof 而不是
    getClass()
    :前者对子类更宽容,也更简洁。若要求「严格同类才算相等」才用
    getClass()
  • equals/hashCode
    用到的字段必须一致
    :上面都用 id。若
    equalsidhashCode
    username,会出现「equals 相等但 hashCode
    不等」这种违反契约的 bug。

2.5 HashMap
遍历的三种方式对比(生产示例)

// MapTraverseDemo.java —— 三种遍历方式的性能与可读性对比
import java.util.HashMap;
import java.util.Map;

public class MapTraverseDemo {
    public static void main(String[] args) {
        Map<Long, User> userMap = new HashMap<>();
        userMap.put(1L, new User(1L, "张三", 25));
        userMap.put(2L, new User(2L, "李四", 30));
        userMap.put(3L, new User(3L, "王五", 28));

        // 方式一(推荐):entrySet —— 一次拿到 key 和 value,无需二次哈希
        for (Map.Entry<Long, User> entry : userMap.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        // 方式二(不推荐):keySet 再 get —— 每次 get 都要再哈希定位一次桶,性能差
        for (Long id : userMap.keySet()) {
            System.out.println(id + " -> " + userMap.get(id));
        }

        // 方式三(JDK8+,最简洁):forEach + Lambda
        userMap.forEach((id, user) -> System.out.println(id + " -> " + user));
    }
}

关键点:遍历 Map 一律用
entrySet()
keySet()
get() 是「二次哈希」,数据量大时性能明显变差,也是 Code
Review 里的常见驳回点。

2.6 遍历中删除:fail-fast
与正确姿势

// RemoveDemo.java —— 遍历时删除元素会抛 ConcurrentModificationException
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class RemoveDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("张三", "李四", "王五"));

        // 错误写法:for-each 里直接 list.remove,会抛 ConcurrentModificationException
        // for (String name : names) {
        //     if ("李四".equals(name)) names.remove(name);
        // }

        // 正确写法一:removeIf(JDK8+,最推荐,语义清晰)
        names.removeIf("李四"::equals);

        // 正确写法二:Iterator.remove(需要边遍历边做更多逻辑时)
        Iterator<String> it = names.iterator();
        while (it.hasNext()) {
            if ("王五".equals(it.next())) {
                it.remove();
            }
        }
        System.out.println(names);   // [张三]
    }
}

fail-fast 机制:ArrayList 遍历时内部记录一个
modCount(修改次数),一旦遍历过程中集合被结构性修改,next()
检测到 modCount 变了就立刻抛
ConcurrentModificationException,宁可快速失败也不返回脏数据。单线程里这是「写错代码」的信号,多线程里这就是
ArrayList 非线程安全的证据(并发写要用
CopyOnWriteArrayList)。

2.7 字符串与金额(两个高频坑)

// StringMoneyDemo.java —— String 三兄弟 + BigDecimal 金额
import java.math.BigDecimal;

public class StringMoneyDemo {
    public static void main(String[] args) {
        // String 不可变;循环拼接会不断创建中间对象,必须用 StringBuilder
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(i).append(',');
        }
        String joined = sb.toString();

        // 错误:new BigDecimal(double) 会引入二进制浮点误差
        BigDecimal wrong = new BigDecimal(0.1);
        System.out.println(wrong);   // 0.1000000000000000055511151231257827021181583404541015625

        // 正确:用字符串构造 或 BigDecimal.valueOf(内部也是字符串构造)
        BigDecimal right = BigDecimal.valueOf(0.1);
        System.out.println(right);   // 0.1

        // 金额比较必须用 compareTo,不能用 equals(equals 会比较 scale 小数位数)
        BigDecimal a = new BigDecimal("10.0");
        BigDecimal b = new BigDecimal("10.00");
        System.out.println(a.equals(b));          // false(scale 不同)
        System.out.println(a.compareTo(b) == 0);  // true(数值相等)
    }
}

String / StringBuilder /
StringBuffer 区别:

类型 可变性 线程安全 场景
String 不可变 —(天然安全) 常量、少量拼接
StringBuilder 可变 循环/大量拼接首选
StringBuffer 可变 是(方法加 synchronized) 几乎不用,性能差

金额运算一律 BigDecimal,构造用字符串或
valueOf,比较用
compareTo。这是支付、订单、库存系统的基本铁律。

2.8 ArrayList 扩容与
LinkedList 的真相

ArrayList 底层是 Object[]
数组,默认初始容量 10,满了扩容为原来的 1.5
oldCapacity + (oldCapacity >> 1)),扩容时用
Arrays.copyOf
复制整个数组——所以频繁扩容是性能杀手,能预估大小就传初始容量

// 生产技巧:预分配容量,避免频繁扩容复制
List<User> users = new ArrayList<>(1000);   // 预估 1000 条,一次到位

LinkedList 底层是双向链表,get(index)
O(n)
从头/尾遍历(会判断靠哪头近),所以它几乎只适合「频繁头尾插入」或「实现队列/栈」的场景——而
Java 提供了更好的
ArrayDeque(循环数组实现的队列),所以生产里
LinkedList 用得很少。

2.9 HashSet
的真相:它就是个「只有 key 的 HashMap」

HashSet 内部持有一个
HashMapadd(e) 实际是
map.put(e, PRESENT)PRESENT 是一个共享的哨兵
Object)。所以:

  • HashSet 去重完全依赖 hashCode() +
    equals()
    (回到 2.4 的契约);
  • HashSet 里放可变对象,若对象被修改导致
    hashCode
    变了,就会「既删不掉也找不到」——这是经典坑。放进 Set/Map 作为
    key 的对象,其 hashCode/equals
    用到的字段应尽量不可变

2.10 LinkedHashMap 做 LRU
缓存

LinkedHashMapHashMap
基础上维护了一条双向链表来记录访问顺序。设
accessOrder=true 后,每次 get
都会把该条目移到链表尾部,于是链表头部就是「最久未访问」——这正是
LRU(Least Recently Used)缓存。

// LruCacheDemo.java —— 用 LinkedHashMap 实现 LRU 缓存(面试高频手写题)
import java.util.LinkedHashMap;
import java.util.Map;

public class LruCacheDemo {
    public static void main(String[] args) {
        // accessOrder=true:按访问顺序排序;重写 removeEldestEntry 实现容量上限
        Map<String, String> lru = new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > 3;          // 超过 3 条就淘汰最久未访问的
            }
        };

        lru.put("a", "1");
        lru.put("b", "2");
        lru.put("c", "3");
        lru.get("a");                        // 访问 a,a 变「最新」
        lru.put("d", "4");                   // 触发淘汰,最久未访问的 b 被移除

        System.out.println(lru.keySet());    // [c, a, d](b 被淘汰)
    }
}

2.11 排序:Comparable 与
Comparator

  • Comparable:对象自身实现
    compareTo,定义「自然顺序」(一个类只能有一种)。
  • Comparator:外部比较器,定义「临时顺序」(可以有多种,如按年龄、按姓名)。
// SortDemo.java —— Comparator 外部排序(生产最常用)
import java.util.*;

public class SortDemo {
    record Person(Long id, String name, Integer age) {}

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>(List.of(
            new Person(2L, "李四", 30),
            new Person(1L, "张三", 25),
            new Person(3L, "王五", 25)
        ));

        // Comparator:按年龄升序,年龄相同按姓名升序(生产常用)
        people.sort(Comparator.comparing(Person::age).thenComparing(Person::name));
        System.out.println(people);

        // 按年龄降序
        people.sort(Comparator.comparing(Person::age).reversed());
        System.out.println(people);
    }
}

2.12 不可变集合:防止外部误改

JDK 9+ 的 List.of / Set.of /
Map.of 创建的是不可变集合,任何修改都会抛
UnsupportedOperationException,适合定义常量集合或「只读视图」:

// ImmutableDemo.java —— 不可变集合
import java.util.*;

public class ImmutableDemo {
    public static void main(String[] args) {
        List<String> names = List.of("张三", "李四");   // 不可变,且不允许 null 元素
        // names.add("王五");                          // 抛 UnsupportedOperationException

        // 把可变集合包成只读视图(内部数据变化仍会反映,但不能通过该视图修改)
        List<String> mutable = new ArrayList<>(List.of("a", "b"));
        List<String> unmodifiable = Collections.unmodifiableList(mutable);
        // unmodifiable.add("c");                      // 抛异常
        System.out.println(unmodifiable);
    }
}

2.13 负载因子
0.75:时间与空间的折中

HashMap 默认负载因子 0.75 是经验最优值:

  • 太大(如
    1.0):桶塞得更满才扩容,空间省了,但哈希碰撞变多,查得慢;
  • 太小(如 0.5):频繁扩容,空间浪费,但碰撞少。

0.75
是「时间开销」和「空间开销」的平衡点。一般不要改它,除非你对数据分布和性能有明确的实测依据。

本章小结

面向对象是 Java 的语言底色;集合选型与 HashMap
原理、equals/hashCode
契约、fail-fast、BigDecimal
金额,构成了生产代码里每天都要用对的基本盘。

第 3 章 泛型、注解与反射

这三样是「看懂 Spring
源码」的三把钥匙:泛型让代码类型安全又复用,注解让框架能「声明式」地给代码加能力,反射让框架能在运行时「读懂并操作」你的类。学完本章,你就明白
Spring 的依赖注入、AOP、MyBatis 的 Mapper 代理底层是怎么回事了。

3.1 泛型:从语法到 PECS

泛型的作用是把类型检查从运行时提前到编译期,同时避免到处强转。基础语法:

// GenericDemo.java —— 泛型类、泛型方法、通配符的最小可用示例
import java.util.ArrayList;
import java.util.List;

public class GenericDemo {

    // 泛型类:类名后声明类型参数,实例化时确定具体类型
    static class Box<T> {
        private T value;
        Box(T value) { this.value = value; }
        T get() { return value; }
        void set(T value) { this.value = value; }
    }

    // 泛型方法:返回值前声明 <T>,让方法独立于类的类型参数
    static <T> T firstOrNull(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // PECS-生产者 extends:只从 list 读元素,不能往里写(因为不知道具体子类型)
    static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) total += n.doubleValue();
        return total;
    }

    // PECS-消费者 super:只往 list 写元素,读出来只能是 Object(因为不知道具体父类型)
    static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
    }

    public static void main(String[] args) {
        Box<String> box = new Box<>("hello");   // 菱形语法,右侧自动推断
        System.out.println(box.get());

        System.out.println(firstOrNull(List.of("a", "b")));   // 编译期保证类型安全

        System.out.println(sum(List.of(1, 2, 3.5)));          // 9.5

        List<Number> nums = new ArrayList<>();
        addIntegers(nums);                                    // List<? super Integer> 可接 List<Number>
        System.out.println(nums);                             // [1, 2]
    }
}

PECS 原则(记住这句口诀:Producer Extends,
Consumer Super
):

  • <? extends T>:这个集合是「生产者」,你只能从里面读(读出来是
    T),不能写。用于「我只读不写」的参数。
  • <? super T>:这个集合是「消费者」,你只能往里面写
    T(或 T 的子类),读出来只能是 Object。用于「我只写不读」的参数。
  • 什么时候同时要读要写?那就别用通配符,用具体类型
    <T>

3.2 类型擦除(重要限制)

泛型是编译期的概念,编译后会被「擦除」:List<String>
List<Integer> 在运行时都是同一个
List(元素类型变成
Object,并在需要处插入强转)。这带来几个硬限制,必须记住:

// ErasureDemo.java —— 类型擦除带来的三个限制(都编译不过,理解即可,不用跑)
public class ErasureDemo {
    // 限制 1:不能 new T()(运行时不知道 T 是什么,无法实例化)
    // static <T> T create() { return new T(); }

    // 限制 2:不能 instanceof T(运行时 T 已被擦除为 Object)
    // static <T> boolean isInstance(Object o) { return o instanceof T; }

    // 限制 3:不能重载「仅泛型参数不同」的方法(擦除后签名相同,冲突)
    // static void print(List<String> list) {}
    // static void print(List<Integer> list) {}

    // 限制 4:泛型不能是基本类型(int 要装箱成 Integer)
    // List<int> list = new ArrayList<>();  // 错误,要用 List<Integer>
}

类型擦除的「补偿」:因为擦除,运行时才知道具体类型的地方(如反序列化),需要显式传递
Class<T>
作为类型令牌。下一节反射的对象↔︎Map
工具就会用到这个技巧。

3.3 注解与元注解

注解本身不做任何事,它只是元数据,需要「反射」或「注解处理器」来读取并赋予行为。元注解用于描述自定义注解

元注解 作用
@Target 注解能加在哪:TYPE(类)、METHOD(方法)、FIELD(字段)、PARAMETER(参数)…
@Retention 注解保留到何时:SOURCE(源码,编译丢弃)、CLASS(字节码,运行丢弃)、RUNTIME(运行时可反射读取)
@Documented 让注解出现在 Javadoc 里
@Inherited 子类继承父类上的注解(只对类注解有效)

生产级自定义注解——幂等注解(实际项目里配合 AOP
做防重复提交、防重复扣款):

// Idempotent.java —— 幂等注解:标记「这个方法需要保证幂等(重复调用结果不变)」
import java.lang.annotation.*;

@Target(ElementType.METHOD)          // 只能加在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时可反射读取(AOP 依赖这个)
@Documented
public @interface Idempotent {
    String key() default "";         // 幂等键的表达式,如 "#orderNo"(SpEL 风格占位)
    long expire() default 10;        // 幂等记录的过期时间,单位秒
}
// OrderService.java —— 使用幂等注解的最小示例
public class OrderService {
    @Idempotent(key = "#orderNo", expire = 60)
    public void createOrder(String orderNo) {
        // 业务逻辑:下单
        System.out.println("创建订单:" + orderNo);
    }
}

3.4 反射:运行时的「照妖镜」

反射允许你在运行时获取类的构造器、方法、字段,甚至调用私有成员(setAccessible(true))。Spring
@Autowired 注入、AOP 切面、MyBatis 的 Mapper
动态代理,底层都是反射。

// ReflectDemo.java —— 反射读取自定义注解 + 调用私有方法
import java.lang.reflect.Method;

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        // 1. 拿到方法对象(这里演示读 OrderService 上的幂等注解)
        Method method = OrderService.class.getMethod("createOrder", String.class);
        Idempotent anno = method.getAnnotation(Idempotent.class);
        System.out.println("幂等键表达式 = " + anno.key() + ",过期秒数 = " + anno.expire());

        // 2. 调用私有方法(生产框架里 setAccessible 很常见)
        Method secret = SecretHolder.class.getDeclaredMethod("secret");
        secret.setAccessible(true);                        // 突破 private 访问限制
        System.out.println(secret.invoke(new SecretHolder()));
    }
}

class SecretHolder {
    private String secret() { return "反射破开了 private"; }
}

3.5 用反射实现「对象 ↔︎
Map」互转(生产级示例)

这是本章的综合实战:很多场景(把对象塞进缓存、做通用日志、序列化前处理)需要把对象转成
Map,再还原。下面是一个处理嵌套对象、跳过
null、按需还原类型
的生产级工具类。

// BeanMapConverter.java —— 反射实现 对象 <-> Map 互转(生产级:异常处理 + 嵌套处理)
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public final class BeanMapConverter {

    // 禁止实例化工具类(私有构造器,阿里手册规范)
    private BeanMapConverter() {}

    /** 对象转 Map:只取非静态字段,key 为字段名,value 为字段值(null 跳过) */
    public static Map<String, Object> toMap(Object obj) {
        if (obj == null) {
            throw new IllegalArgumentException("入参对象不能为 null");
        }
        Map<String, Object> map = new HashMap<>();
        // getDeclaredFields 拿到所有字段(含 private),getFields 只能拿 public
        for (Field field : obj.getClass().getDeclaredFields()) {
            // 跳过静态字段和编译器生成的合成字段,避免污染
            if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) {
                continue;
            }
            try {
                field.setAccessible(true);               // 允许读取 private 字段
                Object value = field.get(obj);
                if (value != null) {                     // 跳过 null,避免 Map 里出现脏 key
                    map.put(field.getName(), value);
                }
            } catch (IllegalAccessException e) {
                // 理论上 setAccessible 后不会发生,防御式兜底
                throw new IllegalStateException("读取字段失败: " + field.getName(), e);
            }
        }
        return map;
    }

    /** Map 转对象:按字段名回填,只设置 Map 里存在的 key */
    public static <T> T fromMap(Map<String, Object> map, Class<T> clazz) {
        if (map == null || clazz == null) {
            throw new IllegalArgumentException("map 与 clazz 均不能为 null");
        }
        try {
            // 类型擦除的补偿:显式传入 Class<T>,运行时才知道要 new 什么
            T instance = clazz.getDeclaredConstructor().newInstance();
            for (Field field : clazz.getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) {
                    continue;
                }
                Object value = map.get(field.getName());
                if (value != null) {
                    field.setAccessible(true);
                    field.set(instance, value);
                }
            }
            return instance;
        } catch (Exception e) {
            // 反射的 checked 异常统一包装成运行时异常,保留原始异常链
            throw new IllegalStateException("Map 转对象失败: " + clazz.getName(), e);
        }
    }
}
// ConverterTest.java —— 验证工具类
import java.util.Map;

public class ConverterTest {
    public static void main(String[] args) {
        User user = new User(1L, "张三", 25);
        Map<String, Object> map = BeanMapConverter.toMap(user);
        System.out.println(map);                          // {id=1, username=张三, age=25}

        User restored = BeanMapConverter.fromMap(map, User.class);
        System.out.println(restored);                     // User{id=1, username='张三', age=25}
        System.out.println(restored.equals(user));        // true(equals 只比 id)
    }
}

运行方式(这两个文件依赖第 2 章的
User.java,放同一目录):

javac -d out User.java BeanMapConverter.java ConverterTest.java
java -cp out ConverterTest

关键点:反射的 getDeclaredConstructor().newInstance()
要求类有无参构造器(User
已提供)。反射有性能开销,Spring
会在启动时做一次、后续复用,而不是每次请求都反射——这是「框架用反射、业务少用反射」的原因。

3.6 动态代理入门

动态代理是 AOP
的底层:在运行时生成一个代理类,拦截对目标对象的方法调用,插入横切逻辑(日志、事务、权限)。

// DynamicProxyDemo.java —— JDK 动态代理:无侵入地给方法加日志
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyDemo {

    interface UserService {
        void save(String name);
    }

    static class UserServiceImpl implements UserService {
        @Override public void save(String name) {
            System.out.println("保存用户: " + name);
        }
    }

    public static void main(String[] args) {
        // 目标对象
        UserService target = new UserServiceImpl();

        // 动态生成代理对象(JDK 代理要求目标实现接口)
        UserService proxy = (UserService) Proxy.newProxyInstance(
            DynamicProxyDemo.class.getClassLoader(),
            new Class<?>[]{UserService.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxyObj, Method method, Object[] args) throws Throwable {
                    System.out.println("[代理] 调用前: " + method.getName());   // 横切逻辑:日志
                    Object result = method.invoke(target, args);               // 调用真实方法
                    System.out.println("[代理] 调用后: " + method.getName());
                    return result;
                }
            }
        );

        proxy.save("张三");
        // 输出:
        // [代理] 调用前: save
        // 保存用户: 张三
        // [代理] 调用后: save
    }
}

关键点:JDK
动态代理只能代理接口UserService),代理类会实现同样的接口;若目标类没有接口,需要用
CGLIB 这类字节码增强(继承目标类生成子类)。Spring AOP 就是「有接口用
JDK 代理、无接口用 CGLIB」这套策略的集大成者。

3.7
更多元注解:@Repeatable 与 @Inherited

  • @Repeatable:让同一个注解在同一处可重复标注(如一个类打多个标签)。
  • @Inherited:父类上的注解,子类(未重新标注时)会自动继承——只对类注解生效。
// RepeatableDemo.java —— 可重复注解
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Tags.class)              // 声明容器注解,才能重复标注
@interface Tag { String value(); }

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Tags { Tag[] value(); }   // 容器注解:持有多个 Tag

@Tag("缓存")
@Tag("核心")
class Article {}

public class RepeatableDemo {
    public static void main(String[] args) {
        for (Tag t : Article.class.getAnnotationsByType(Tag.class)) {
            System.out.println(t.value());   // 缓存 / 核心
        }
    }
}

3.8
类型令牌:泛型擦除的补偿手段

因为类型擦除,运行时拿不到泛型实参。解决办法是显式传递
Class<T>
作为「类型令牌」
,让方法知道要处理的具体类型。第 3.5 节的
BeanMapConverter.fromMap(Map, Class<T>)
就是典型用法。更常见的场景是「从 JSON 反序列化到指定类型」:

// TypeTokenDemo.java —— Class<T> 类型令牌
import java.util.HashMap;
import java.util.Map;

public class TypeTokenDemo {
    // 调用方通过 Class<T> 告诉方法「我要什么类型」
    static <T> T fromMap(Map<String, Object> map, Class<T> clazz) throws Exception {
        T obj = clazz.getDeclaredConstructor().newInstance();
        // ... 反射回填字段(完整实现见 3.5 节 BeanMapConverter)
        return obj;
    }

    public static void main(String[] args) throws Exception {
        Map<String, Object> data = new HashMap<>();
        data.put("id", 1L);
        User user = fromMap(data, User.class);   // 类型令牌 User.class(User 来自第 2 章)
        System.out.println(user);
    }
}

关键点:这就是很多框架(Jackson、Gson)反序列化 API
的底层套路——readValue(json, User.class) 里的
User.class 就是在传类型令牌。

本章小结

泛型(含 PECS
与类型擦除)、注解(含元注解与自定义幂等注解)、反射(含对象↔︎Map
互转与动态代理)是框架级代码的三大支柱,理解了它们,Spring
源码的大门就推开了一半。

第 4 章 Lambda、Stream 与
Optional

这是让 Java 代码从「啰嗦」变「简洁」的一章。Stream
是声明式数据处理:你描述「要什么」,而不是手写「怎么一步步做」。Optional
则把「可能为空」这个事实显式地写进类型签名,逼着调用方处理空值。三者加在一起,能让集合处理代码少写一半、错少出一半。

4.1 函数式接口与 Lambda

函数式接口:只有一个抽象方法的接口,可用
@FunctionalInterface 标注(编译器会强制校验)。Lambda
表达式就是这种接口的一个匿名实现的语法糖。

// LambdaDemo.java —— 函数式接口与 Lambda 的四种写法
public class LambdaDemo {

    @FunctionalInterface
    interface Calculator {
        int calc(int a, int b);
    }

    public static void main(String[] args) {
        // 写法一:完整 Lambda(参数类型可省略)
        Calculator add = (int a, int b) -> a + b;
        // 写法二:省略参数类型
        Calculator sub = (a, b) -> a - b;
        // 写法三:方法体有多行时用大括号
        Calculator mul = (a, b) -> {
            System.out.println("乘法运算");
            return a * b;
        };
        // 写法四:方法引用(已有方法直接引用,最简洁)
        Calculator max = Math::max;

        System.out.println(add.calc(1, 2));   // 3
        System.out.println(sub.calc(5, 2));   // 3
        System.out.println(mul.calc(2, 3));   // 6
        System.out.println(max.calc(1, 2));   // 2
    }
}

JDK 内置的四大函数式接口(配合 Stream 高频使用):

接口 签名 用途
Function<T, R> R apply(T t) 转换:输入 T 输出 R(map 用)
Predicate<T> boolean test(T t) 判断:返回布尔(filter 用)
Consumer<T> void accept(T t) 消费:只进不出(forEach 用)
Supplier<T> T get() 生产:只出不进(Optional.orElseGet 用)

4.2 Stream 三阶段与惰性求值

Stream
的完整流程分三阶段,只有终止操作会真正触发计算

  1. 创建list.stream()(串行)/
    list.parallelStream()(并行);
  2. 中间操作filter / map /
    sorted / distinct /
    limit…(惰性,不立即执行,返回新
    Stream);
  3. 终止操作collect /
    forEach / count /
    reduce…(触发执行,流关闭)。
// StreamDemo.java —— 三阶段与惰性验证
import java.util.List;

public class StreamDemo {
    public static void main(String[] args) {
        List<String> names = List.of("张三", "李四", "王五", "赵六");

        // 惰性验证:去掉终止操作,中间操作不会执行(下面这行没有任何输出)
        names.stream().filter(n -> {
            System.out.println("filter 执行了: " + n);
            return n.length() == 2;
        });

        System.out.println("---- 加上终止操作后 ----");

        // 加上终止操作 forEach,中间操作才会真正执行
        names.stream()
            .filter(n -> n.length() == 2)          // 中间:过滤
            .map(n -> "姓名:" + n)                 // 中间:转换
            .forEach(System.out::println);         // 终止:消费

        // 输出:
        // ---- 加上终止操作后 ----
        // 姓名:张三
        // 姓名:李四
        // 姓名:王五
        // 姓名:赵六
    }
}

4.3
常见操作:filter / map / collect / groupingBy

// CollectDemo.java —— 常用终止操作:toList / toMap / groupingBy / 统计
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class CollectDemo {

    static class User {
        Long id; String name; Integer age;
        User(Long id, String name, Integer age) {
            this.id = id; this.name = name; this.age = age;
        }
        Long getId() { return id; }
        String getName() { return name; }
        Integer getAge() { return age; }
        @Override public String toString() { return "User{id=" + id + ", name='" + name + "', age=" + age + '}'; }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User(1L, "张三", 25),
            new User(2L, "李四", 30),
            new User(3L, "王五", 25),
            new User(4L, "赵六", 30)
        );

        // 1. filter + collect(toList):过滤年龄 > 25
        List<User> adults = users.stream()
            .filter(u -> u.getAge() > 25)
            .collect(Collectors.toList());
        System.out.println("年龄>25: " + adults);

        // 2. map + collect(toList):只取姓名
        List<String> names = users.stream()
            .map(User::getName)
            .collect(Collectors.toList());
        System.out.println("姓名列表: " + names);

        // 3. groupingBy:按年龄分组
        Map<Integer, List<User>> byAge = users.stream()
            .collect(Collectors.groupingBy(User::getAge));
        System.out.println("按年龄分组: " + byAge);

        // 4. 统计:数量、平均年龄、最大年龄
        long count = users.stream().count();
        double avgAge = users.stream().mapToInt(User::getAge).average().orElse(0);
        int maxAge = users.stream().mapToInt(User::getAge).max().orElse(0);
        System.out.println("人数=" + count + ", 平均年龄=" + avgAge + ", 最大年龄=" + maxAge);
    }
}

4.4 List 转
Map(防键重复,生产级高频坑)

Collectors.toMap 在 key 重复时会直接抛
IllegalStateException
。生产数据几乎不可能保证 key
唯一(比如按年龄做
key),所以必须传第三个参数(合并函数)兜底

// ToMapDemo.java —— List 转 Map 的键重复问题
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ToMapDemo {
    public static void main(String[] args) {
        List<CollectDemo.User> users = List.of(
            new CollectDemo.User(1L, "张三", 25),
            new CollectDemo.User(2L, "李四", 30),
            new CollectDemo.User(3L, "王五", 25)   // 年龄 25 出现两次
        );

        // 错误:按年龄做 key,会抛 IllegalStateException: Duplicate key
        // Map<Integer, CollectDemo.User> byAge = users.stream()
        //     .collect(Collectors.toMap(CollectDemo.User::getAge, Function.identity()));

        // 正确:用合并函数 (old, fresh) -> fresh,key 冲突时保留新值
        Map<Integer, CollectDemo.User> byAgeKeepNew = users.stream()
            .collect(Collectors.toMap(
                CollectDemo.User::getAge,
                Function.identity(),
                (oldValue, newValue) -> newValue       // 保留后出现的值
            ));
        System.out.println("保留新值: " + byAgeKeepNew);

        // 生产常用:按唯一 id 转 Map(id 唯一,合并函数只是防御性兜底)
        Map<Long, CollectDemo.User> byId = users.stream()
            .collect(Collectors.toMap(CollectDemo.User::getId, Function.identity(), (a, b) -> a));
        System.out.println("按 id 转 Map: " + byId);

        // 分组统计:每个年龄有多少人(groupingBy + counting)
        Map<Integer, Long> ageCount = users.stream()
            .collect(Collectors.groupingBy(CollectDemo.User::getAge, Collectors.counting()));
        System.out.println("各年龄人数: " + ageCount);
    }
}

关键点:toMap
的第三个合并函数是「键冲突时的裁决规则」:(a, b) -> a
保留旧值,(a, b) -> b
保留新值,(a, b) -> a + b 求和。
toMap 不写第三个参数 = 埋雷

4.5 Optional 正确用法

Optional
的本质是把「可能为空」写进类型签名:一个方法返回
Optional<User>,就是在告诉调用方「我可能查不到,你看着办」。这比返回
null 安全得多。

// OptionalDemo.java —— Optional 的正确与错误用法
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class OptionalDemo {

    static class UserService {
        // 模拟按 id 查询,可能查不到
        private static final Map<Long, String> DB = new HashMap<>();
        static { DB.put(1L, "张三"); }

        // 正确:返回 Optional<User>,显式表达「可能为空」
        Optional<String> findById(Long id) {
            return Optional.ofNullable(DB.get(id));
        }
    }

    public static void main(String[] args) {
        UserService service = new UserService();

        // 正确写法一:orElseThrow —— 查不到就抛业务异常(推荐,链式)
        String user = service.findById(1L)
            .orElseThrow(() -> new IllegalArgumentException("用户不存在"));
        System.out.println(user);

        // 正确写法二:orElse / orElseGet —— 给默认值
        String fallback = service.findById(999L).orElse("匿名用户");
        System.out.println(fallback);

        // 正确写法三:ifPresent —— 存在才执行
        service.findById(1L).ifPresent(u -> System.out.println("找到: " + u));

        // 正确写法四:map + filter 链式转换
        String upper = service.findById(1L)
            .map(String::toUpperCase)
            .filter(s -> s.length() > 1)
            .orElse("默认");
        System.out.println(upper);

        // 错误写法:isPresent + get(等价于判 null 再取值,失去 Optional 意义)
        // if (service.findById(1L).isPresent()) {
        //     String u = service.findById(1L).get();
        // }
    }
}

三条 Optional 铁律(阿里手册):

  1. 只作为方法返回值表达「可能为空」,禁止当字段、禁止当方法入参。
  2. 禁止 isPresent() + get(),用
    orElseThrow / orElse / ifPresent
    代替——前者和「判 null 再取值」一样啰嗦且易错。
  3. 慎用
    orElse(固定值)
    orElse
    的参数无论是否为空都会先求值,若默认值是昂贵操作(如查库、new
    大对象),要用 orElseGet(() -> ...) 惰性求值。

4.6 parallelStream 的性能陷阱

parallelStream 用 ForkJoin
线程池并行处理,不是万能加速器。下面三种情况用了反而更慢:

  • 数据量小:切分任务、线程调度的开销大于并行收益;
  • 有 IO 操作:并行 IO 会打满连接、加剧资源竞争;
  • 有顺序/依赖findFirstsorted
    等强依赖顺序的操作,并行要额外同步。
// ParallelDemo.java —— parallelStream 不是银弹(数据量小时串行更快)
import java.util.List;
import java.util.stream.IntStream;

public class ParallelDemo {
    public static void main(String[] args) {
        List<Integer> small = IntStream.rangeClosed(1, 1000).boxed().toList();
        List<Integer> large = IntStream.rangeClosed(1, 10_000_000).boxed().toList();

        // 数据量小:串行通常更快(并行有线程调度开销)
        long t1 = System.currentTimeMillis();
        long s1 = small.stream().mapToLong(Integer::longValue).sum();
        long t2 = System.currentTimeMillis();
        long s2 = small.parallelStream().mapToLong(Integer::longValue).sum();
        long t3 = System.currentTimeMillis();
        System.out.println("小数据 串行=" + (t2 - t1) + "ms, 并行=" + (t3 - t2) + "ms");

        // 数据量大:并行才有明显优势
        long t4 = System.currentTimeMillis();
        long l1 = large.stream().mapToLong(Integer::longValue).sum();
        long t5 = System.currentTimeMillis();
        long l2 = large.parallelStream().mapToLong(Integer::longValue).sum();
        long t6 = System.currentTimeMillis();
        System.out.println("大数据 串行=" + (t5 - t4) + "ms, 并行=" + (t6 - t5) + "ms");
    }
}

关键点:默认用
stream()
,只有在「数据量大、纯 CPU
计算、无顺序依赖」且实测确实变快时,才用
parallelStream()。并行流用的是公共
ForkJoinPool,滥用会拖垮整个 JVM 的其他并行任务。

4.7 flatMap 与 reduce

flatMap
把「流里的每个元素」映射成一个流,再拍平成一个流,常用于「嵌套集合展开」:

// FlatMapDemo.java —— flatMap 拍平嵌套集合
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapDemo {
    public static void main(String[] args) {
        // 一个用户有多个订单号,需求:取出所有用户的全部订单号(去重)
        record UserOrders(String name, List<String> orders) {}
        List<UserOrders> list = List.of(
            new UserOrders("张三", List.of("O1", "O2")),
            new UserOrders("李四", List.of("O2", "O3"))
        );

        List<String> allOrders = list.stream()
            .flatMap(u -> u.orders().stream())   // 每个 UserOrders 展开成订单流,再拍平
            .distinct()                          // 去重 O2
            .collect(Collectors.toList());
        System.out.println(allOrders);           // [O1, O2, O3]
    }
}

reduce 把流「归约」成一个值(求和、求积):

// ReduceDemo.java —— reduce 归约
import java.util.List;

public class ReduceDemo {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5);
        int sum = nums.stream().reduce(0, Integer::sum);        // 求和 = 15
        int product = nums.stream().reduce(1, (a, b) -> a * b); // 求积 = 120
        System.out.println("sum=" + sum + ", product=" + product);
    }
}

4.8 分区与统计汇总

// PartitionDemo.java —— partitionBy 二分 + summarizingInt 汇总
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class PartitionDemo {
    record Person(String name, int age) {}

    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("张三", 25), new Person("李四", 15),
            new Person("王五", 30), new Person("赵六", 10)
        );

        // partitionBy:按谓词二分(成年/未成年)
        Map<Boolean, List<Person>> adults = people.stream()
            .collect(Collectors.partitioningBy(p -> p.age() >= 18));
        System.out.println("成年人: " + adults.get(true));
        System.out.println("未成年: " + adults.get(false));

        // summarizingInt:一次拿到 count/sum/min/max/average
        var summary = people.stream().collect(Collectors.summarizingInt(Person::age));
        System.out.println("人数=" + summary.getCount()
            + ", 平均年龄=" + summary.getAverage()
            + ", 最大=" + summary.getMax());
    }
}

4.9
Optional.flatMap:链式处理「可能为空的可能为空」

当链上的某一步又返回 Optional(嵌套 Optional)时,用
flatMap 拍平,避免
Optional<Optional<T>>

// OptionalFlatMapDemo.java —— flatMap 拍平嵌套 Optional
import java.util.Optional;

public class OptionalFlatMapDemo {
    public static void main(String[] args) {
        Optional<Optional<String>> nested = Optional.of(Optional.of("hello"));
        // map 会得到 Optional<Optional<String>>,flatMap 拍平成 Optional<String>
        String s = nested.flatMap(o -> o).orElse("空");
        System.out.println(s);
    }
}

本章小结

函数式接口 + Lambda 让代码变短,Stream
三阶段(创建→中间→终止)让数据处理变声明式,Optional
让空值处理变显式;记住 toMap
必带合并函数、Optional
只做返回值、parallelStream 谨慎用这三条铁律。

第 5 章 异常处理与日志

生产排查问题全靠异常信息和日志。乱 catch
会吞掉线索,不记日志等于摸黑排障。本章建立两套生产级设施:一套「错误码 +
业务异常」体系,一套「SLF4J + Logback」日志体系。其中
ErrorCodeBizException 这两个类,会被后续所有
Spring Boot 阶段逐字复用,请照抄并记住。

5.1 受检异常 vs 非受检异常

Java 异常分两大类:

  • 受检异常(Checked):继承自
    Exception(但不含
    RuntimeException),编译器强制你必须
    try-catchthrows
    声明。典型:IOExceptionSQLException。设计意图是「这些错误可以恢复,调用方必须处理」。
  • 非受检异常(Unchecked):继承自
    RuntimeException,编译器不强制处理。典型:NullPointerExceptionIllegalArgumentException。代表「编程错误或无法恢复的业务错误」。

生产实践:业务异常统一用非受检(RuntimeException
子类)
,这样不用在每个方法签名上 throws
满天飞,由全局异常处理器统一兜底(Spring 阶段的
GlobalExceptionHandler 就是干这个的)。

5.2
自定义异常体系(生产级核心)

一个可维护的异常体系包含两层:错误码枚举(定义所有可预期的错误)+
业务异常类(携带错误码抛出)。

// ErrorCode.java —— 统一错误码枚举(公共类,后续阶段逐字复用,基础码不得改动)
public enum ErrorCode {
    SUCCESS(0, "success"),
    PARAM_ERROR(40001, "参数错误"),
    UNAUTHORIZED(40101, "未登录或登录已过期"),
    FORBIDDEN(40301, "无权限访问"),
    USER_NOT_FOUND(40401, "用户不存在"),
    USER_EXISTS(40002, "用户名已存在"),
    SYSTEM_ERROR(50000, "系统繁忙,请稍后重试"),
    ;

    private final int code;
    private final String message;

    // 枚举构造器默认 private,无需显式写
    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
}
// BizException.java —— 统一业务异常(公共类,后续阶段逐字复用)
public class BizException extends RuntimeException {

    private final int code;

    public BizException(ErrorCode ec) {
        super(ec.getMessage());   // 错误码里的 message 作为异常信息
        this.code = ec.getCode();
    }

    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() { return code; }
}

设计说明:错误码用 int(而非
String),便于前后端对齐、排查时快速检索。4xxxx
是客户端可修复的错误(参数错、未登录、无权限、资源不存在),5xxxx
是服务端错误。这套语义和 HTTP 状态码一致,方便你后续对接 Spring
的响应体。

使用示例:

// BizDemo.java —— 抛出与捕获业务异常
public class BizDemo {
    public static void main(String[] args) {
        try {
            registerUser("张三");
            registerUser("张三");   // 第二次触发「用户名已存在」
        } catch (BizException e) {
            // 打印业务码 + 业务提示,而不是堆栈
            System.out.println("业务失败: code=" + e.getCode() + ", msg=" + e.getMessage());
        }
    }

    static void registerUser(String name) {
        if ("张三".equals(name)) {
            // 抛业务异常:可预期的错误,用错误码表达
            throw new BizException(ErrorCode.USER_EXISTS);
        }
        System.out.println("注册成功: " + name);
    }
}

运行:javac ErrorCode.java BizException.java BizDemo.java && java BizDemo

5.3 异常链与禁止事项

异常链:捕获底层异常后,用带 cause
的构造器把原始异常挂上去,保留完整堆栈。生产排查时日志会打出整条链,定位根因就靠它。

为此,BizException 需要额外提供一个带
cause
的重载构造器
(在总规范原始定义上新增,不改变既有公共签名,后续阶段同样复用):

// BizException 追加构造器(在原有 (ErrorCode)/(int,String) 两个构造器基础上新增)
public BizException(int code, String message, Throwable cause) {
    super(message, cause);   // cause 挂到父类,getCause() 能拿到
    this.code = code;
}
// ExceptionChainDemo.java —— 异常链:包装底层异常并保留 cause
public class ExceptionChainDemo {
    public static void main(String[] args) {
        try {
            loadConfig();
        } catch (BizException e) {
            // getCause() 拿到被包装的原始异常,整条链定位根因
            System.out.println("业务异常: " + e.getMessage());
            System.out.println("根因: " + e.getCause());
        }
    }

    static void loadConfig() {
        try {
            // 模拟底层 IO 异常
            throw new java.io.IOException("配置文件不存在");
        } catch (java.io.IOException e) {
            // 包装成业务异常,同时用第三个参数保留原始异常链
            throw new BizException(ErrorCode.SYSTEM_ERROR.getCode(), "配置加载失败", e);
        }
    }
}

运行:javac ErrorCode.java BizException.java ExceptionChainDemo.java && java ExceptionChainDemo,输出里
根因: 会打印
java.io.IOException: 配置文件不存在,证明异常链完整保留了。

生产异常的四条禁止(阿里手册):

  1. 禁止空 catchcatch (Exception e) {}
    吞掉异常,线上问题成「无头案」;
  2. 禁止 e.printStackTrace():直接打到
    stderr,不走日志框架,生产会丢失;
  3. 禁止
    catch (Exception e) { return null; }
    :让调用方拿到
    null 继续跑,错误被掩盖;
  4. 禁止在循环里
    try-catch
    :性能差且吞异常,应把异常上抛,在循环外统一处理。

5.4
try-with-resources(自动关资源)

凡是实现了 AutoCloseable 的资源(流、连接),都应该用
try-with-resources,JVM 自动在 finally 里帮你
close
,避免资源泄漏。

// TryWithResourcesDemo.java —— 自动关闭资源
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesDemo {
    public static void main(String[] args) {
        // 资源在 try 括号里声明,代码块结束后自动 close(倒序关闭)
        try (BufferedReader reader = new BufferedReader(new FileReader("pom.xml"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("读取失败: " + e.getMessage());
        }
    }
}

5.5 日志:SLF4J + Logback

为什么用 SLF4J + Logback:SLF4J
日志门面(接口),Logback
实现。业务代码只依赖 SLF4J
接口,具体用哪个实现可以在部署时切换(Logback / Log4j2
等),这就是「面向接口编程」在日志上的应用。Java 自带的
java.util.logging(JUL)功能弱、配置麻烦,生产基本不用。

最小可用(零依赖,先建立概念)

// JulDemo.java —— JDK 自带日志,零依赖即可运行(仅用于理解概念)
import java.util.logging.Logger;

public class JulDemo {
    private static final Logger log = Logger.getLogger(JulDemo.class.getName());

    public static void main(String[] args) {
        log.info("这是一条 info 日志");
        log.warning("这是一条 warning 日志");
    }
}

生产级(SLF4J +
Logback)
:需要引入依赖,这里先给一个最小 pom.xml
让你能跑通(Maven 的完整讲解在第 7 章,现在照抄即可)。

<!-- pom.xml —— 最小可运行配置,仅引入日志依赖(完整 Maven 讲解见第 7 章) -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>logging-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- SLF4J 门面 API -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.13</version>
        </dependency>
        <!-- Logback 实现(会传递引入 slf4j-api,但显式声明更清晰) -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.6</version>
        </dependency>
    </dependencies>
</project>
<!-- src/main/resources/logback.xml —— Logback 基础配置 -->
<configuration>
    <!-- 控制台输出器 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志格式:时间 级别 线程名 类名 - 消息 -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 根日志级别:INFO 及以上才输出(DEBUG 太多,生产默认 INFO) -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>

    <!-- 对指定包调级别:本包 DEBUG,方便排查时临时打开 -->
    <logger name="com.example" level="DEBUG"/>
</configuration>
// src/main/java/com/example/LogDemo.java —— 生产级日志写法
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogDemo {

    // 每个类一个 Logger,参数传当前类,便于日志定位到具体类
    private static final Logger log = LoggerFactory.getLogger(LogDemo.class);

    public static void main(String[] args) {
        log.info("用户登录成功, userId={}", 10086);          // 占位符写法,避免字符串拼接开销
        log.debug("这是一条调试日志,生产默认不输出");

        try {
            int r = 1 / 0;
        } catch (Exception e) {
            // 异常必须带堆栈一起打(第二个参数传异常对象),否则丢失排查线索
            log.error("计算失败, userId={}", 10086, e);
        }
    }
}

运行(先确保第 7 章的 Maven 概念不熟也没关系,直接执行):

mvn compile exec:java -Dexec.mainClass="com.example.LogDemo"
# 或先 mvn package 再 java -cp target/classes:$HOME/.m2/repository/... 运行

关键点:

  1. 日志用 {} 占位符,不要
    "用户" + id
    字符串拼接——占位符只在「该级别需要输出」时才真正拼字符串,DEBUG
    关闭时零开销;
  2. 异常用 log.error("...", e)
    传异常对象
    ,让 Logback 自动打印堆栈;
  3. 生产默认 INFO,DEBUG
    只在排查时对指定包临时打开(logback.xml
    <logger> 已演示);
  4. 全系列规范:业务代码禁止
    System.out
    ,一律用 Logger(Spring
    阶段会用 Lombok 的 @Slf4j 省掉这行声明)。

5.6 日志级别与文件滚动

日志级别从低到高:TRACE < DEBUG < INFO < WARN < ERROR生产默认
INFO
,即只输出 INFO 及以上的日志;DEBUG
级别的内容(如方法入参)在排查时才临时打开。

生产日志不能只打控制台,还要滚动写入文件(按大小/日期切割,便于归档与检索):

<!-- logback.xml 追加:滚动文件输出器(与 CONSOLE 并存) -->
<configuration>
    <!-- 按大小+日期滚动:单文件 10MB,保留 7 天 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

关键点:控制台输出方便开发调试,文件滚动方便生产归档与事后排查。生产环境两个
appender
都要配
,排查时先看文件日志(控制台日志在容器重启后就丢了)。

本章小结

ErrorCode + BizException
建立可预期错误的统一表达,用「异常链」保留根因,用 try-with-resources
自动关资源,用 SLF4J + Logback
记录可检索的日志——这四件事合起来,就是「线上出了问题能快速定位」的底气。

第 6 章 并发编程与线程池

并发是后端的分水岭:单线程写对了只是及格,多线程写对了才算能扛生产流量。本章先把三个核心概念(可见性、有序性、原子性)讲透,再落到生产最常用的两件事上——手动配置线程池
ThreadLocal 传 traceId

6.1 并发三性问题与三个关键词

多线程出错,本质是三个问题:可见性(一个线程改的值,另一个线程看不到)、有序性(指令重排导致执行顺序错乱)、原子性(一段操作被打断)。Java
用三个关键词分别应对:

关键词 保证什么 不保证什么
volatile 可见性 + 有序性(内存屏障禁止重排) 原子性i++ 仍是三步:读-改-写)
synchronized 原子性 + 可见性 + 有序性(互斥 + 内存屏障) —(性能较差,JDK6+ 有锁升级优化)
CASAtomic* 类) 原子性(无锁,CPU 指令级比较交换) 不解决 ABA 问题(需 AtomicStampedReference
// VolatileDemo.java —— volatile 保证可见性,但不保证原子性
public class VolatileDemo {

    // 用 volatile:主线程改 flag,子线程能立刻看到(可见性)
    private static volatile boolean running = true;
    // 不用 volatile:counter++ 是「读-改-写」三步,多线程会丢更新
    private static int counter = 0;
    // 用 AtomicInteger:CAS 保证 ++ 的原子性
    private static final java.util.concurrent.atomic.AtomicInteger atomicCounter =
        new java.util.concurrent.atomic.AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 演示可见性:子线程循环读 running,主线程 1 秒后改 false
        Thread watcher = new Thread(() -> {
            while (running) { /* 空转,等待 running 变 false */ }
            System.out.println("子线程看到 running=false,退出");
        });
        watcher.start();
        Thread.sleep(1000);
        running = false;                 // 若 running 无 volatile,子线程可能永远看不到
        watcher.join();

        // 演示原子性:10 个线程各加 1000 次
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter++;                        // 非原子,结果 < 10000
                    atomicCounter.incrementAndGet();  // CAS 原子,结果 = 10000
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();

        System.out.println("普通 int counter = " + counter);              // 大概率 < 10000
        System.out.println("AtomicInteger counter = " + atomicCounter.get()); // 恒为 10000
    }
}

关键点:volatile
只能保证「读到的值是最新的」,不能保证「读-改-写」三步不被穿插。要修
counter++,要么 synchronized,要么用
AtomicInteger(底层 CAS)。

6.2 synchronized 与锁升级

synchronized 是 Java 内置锁。JDK6
之后做了锁升级优化,让它不再是「一上来就重量级」:

无锁 → 偏向锁(只有一个线程反复进入,无竞争,只做标记)→
轻量级锁(少量竞争,CAS 自旋)→
重量级锁(竞争激烈,挂起线程交给操作系统)。

你只需记住结论:竞争不激烈时 synchronized
性能已经很好
,不必一上来就排斥它;需要更灵活的「可中断、可超时、公平锁」时,才用
ReentrantLock

// SyncDemo.java —— synchronized 方法锁与代码块锁(this 锁)
public class SyncDemo {
    private int balance = 100;

    // 方法锁:锁的是 this 对象
    public synchronized void deposit(int amount) {
        balance += amount;
    }

    // 代码块锁:锁 this(等价于上面的方法锁),粒度更可控
    public void withdraw(int amount) {
        synchronized (this) {
            balance -= amount;
        }
    }

    public int getBalance() { return balance; }
}

6.3 线程池:为什么禁用
Executors(重点)

为什么禁用 Executors
工厂方法
?因为:

  • Executors.newFixedThreadPool(n)
    用的是无界队列
    LinkedBlockingQueue,任务堆积时队列无限增长 →
    OOM
  • Executors.newCachedThreadPool() 的最大线程数是
    Integer.MAX_VALUE,来多少任务开多少线程 → 线程爆炸
    / OOM

所以阿里手册强制:线程池一律用
ThreadPoolExecutor 显式构造,明确 7 个参数

ThreadPoolExecutor 的 7 个参数(面试必背):

new ThreadPoolExecutor(
    corePoolSize,        // 核心线程数:常驻线程,任务来了先由它们执行
    maximumPoolSize,     // 最大线程数:队列满了才开新线程,最多开到这么多
    keepAliveTime,       // 非核心线程空闲多久被回收
    TimeUnit.SECONDS,    // keepAliveTime 的时间单位
    workQueue,           // 阻塞队列:核心线程忙时,任务先进队列排队
    threadFactory,       // 线程工厂:给线程起名(排查时一眼看出是哪个池)
    handler              // 拒绝策略:队列满 + 线程也满时,新任务怎么处理
);

执行流程(必须能口述):任务来 → 先看核心线程是否满 →
没满开核心线程 → 满了进队列 → 队列也满 → 开非核心线程直到最大线程数 →
还是满 → 走拒绝策略

四种拒绝策略:

策略 行为 适用场景
AbortPolicy(默认) RejectedExecutionException 需要感知「我扛不住了」的场景
CallerRunsPolicy 让提交任务的线程自己执行 限流:让上游慢下来,生产常用
DiscardPolicy 静默丢弃新任务 可丢弃的非关键任务(慎用)
DiscardOldestPolicy 丢弃队列最老任务 优先保证新任务(慎用)

6.4 生产级线程池配置类 +
命名工厂

// NamedThreadFactory.java —— 自定义线程工厂:给线程起有业务含义的名字
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class NamedThreadFactory implements ThreadFactory {
    private final String namePrefix;                 // 如 "biz-pool"
    private final AtomicInteger seq = new AtomicInteger(0);

    public NamedThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        // 名字形如 biz-pool-1、biz-pool-2,排查时一眼定位是哪个池的线程
        Thread t = new Thread(r, namePrefix + "-" + seq.incrementAndGet());
        t.setDaemon(false);                          // 业务线程池用非守护线程
        return t;
    }
}
// ThreadPoolDemo.java —— 生产级线程池:显式 7 参数 + 有界队列 + 拒绝策略 + 命名
import java.util.concurrent.*;

public class ThreadPoolDemo {

    public static void main(String[] args) throws InterruptedException {
        // 生产级线程池:CPU 密集型用「核数+1」,IO 密集型用「核数*2」
        int core = Runtime.getRuntime().availableProcessors();   // CPU 核数
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            core,                              // 核心线程数
            core * 2,                          // 最大线程数
            60, TimeUnit.SECONDS,              // 非核心线程空闲 60s 回收
            new ArrayBlockingQueue<>(1000),    // 有界队列,防止 OOM(关键!)
            new NamedThreadFactory("biz-pool"),// 自定义命名工厂
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:让调用线程执行,实现限流
        );

        // 提交任务:业务代码用 execute(无返回值)或 submit(有返回值)
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            pool.execute(() -> {
                String thread = Thread.currentThread().getName();
                System.out.println("任务 " + taskId + " 由线程 [" + thread + "] 执行");
            });
        }

        // 打印池监控指标(生产可接入监控系统)
        System.out.println("核心线程数=" + pool.getCorePoolSize()
            + ", 活跃线程数=" + pool.getActiveCount()
            + ", 队列大小=" + pool.getQueue().size());

        // 优雅关闭:不再接收新任务,等已提交任务执行完(生产停机必须做)
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
    }
}

运行:javac NamedThreadFactory.java ThreadPoolDemo.java && java ThreadPoolDemo

关键点:

  1. 队列必须是有界的ArrayBlockingQueue),这是防
    OOM 的命门;
  2. 核心/最大线程数公式只是经验值:CPU 密集型 = 核数 +
    1,IO 密集型 = 核数 × 2(更精确要压测);
  3. 线程必须命名,线上 jstack
    看线程名就知道是哪个业务池卡了;
  4. 禁止在方法里
    new Thread()
    ,统一走线程池,否则线程失控。

6.5 ThreadLocal:传递
traceId 与内存泄漏

ThreadLocal
是「线程本地变量」:每个线程持有自己的一份拷贝,互不干扰。生产最典型的用途是在链路里传递
traceId
——一次请求从入口到出口,所有日志都打上同一个
traceId,方便串起整条调用链。

// TraceContext.java —— ThreadLocal 工具类:传递 traceId(生产级)
public final class TraceContext {

    private static final ThreadLocal<String> TRACE = new ThreadLocal<>();

    private TraceContext() {}               // 工具类私有构造器

    public static void set(String traceId) { TRACE.set(traceId); }
    public static String get() { return TRACE.get(); }
    public static void clear() { TRACE.remove(); }   // 必须调用,否则内存泄漏
}
// ThreadLocalDemo.java —— 传递 traceId + finally 里 remove(防内存泄漏)
import java.util.concurrent.*;

public class ThreadLocalDemo {

    private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
        2, 2, 60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10),
        new NamedThreadFactory("trace-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            final String traceId = "trace-" + i;
            POOL.execute(() -> {
                try {
                    TraceContext.set(traceId);              // 进入线程先 set
                    handle();                                // 业务逻辑里 get 到同一个 traceId
                } finally {
                    TraceContext.clear();                    // 必须 finally remove,防泄漏
                }
            });
        }
        POOL.shutdown();
    }

    static void handle() {
        System.out.println("线程[" + Thread.currentThread().getName()
            + "] 处理请求, traceId=" + TraceContext.get());
    }
}

为什么必须
remove()
:线程池里的线程是复用的。上一个请求
set 的 traceId 若不
remove,会残留在线程里,下一个复用该线程的请求就读到了上一个请求的
traceId
——这正是线上「用户 A 日志里出现用户 B 的
traceId」这种诡异 bug 的根因。同时,ThreadLocal 的 key
是弱引用、value 是强引用,不 remove 时 value 无法被
GC,长期累积就是内存泄漏

关键点:setremove 必须成对出现,且
remove 放在 finally 里,保证异常时也执行。

6.6
并发工具类:CountDownLatch / Semaphore

JDK 提供了几个高频并发工具,底层都是 AQS:

  • CountDownLatch一次性门闩,等待 N
    个任务完成后放行(如「等所有子任务做完再继续」)。
  • Semaphore信号量,控制同时访问某资源的线程数(如限流、连接池)。

演示并发原语时用裸 new Thread()
是为了聚焦原语语义;生产业务代码禁止裸
new Thread(),统一走线程池(见 6.4)。

// LatchSemaphoreDemo.java —— CountDownLatch 与 Semaphore
import java.util.concurrent.*;

public class LatchSemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        // CountDownLatch:3 个子任务完成后主线程才继续
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            final int id = i;
            new Thread(() -> {
                System.out.println("子任务 " + id + " 完成");
                latch.countDown();                    // 计数 -1
            }).start();
        }
        latch.await();                                // 阻塞直到计数归零
        System.out.println("所有子任务完成,主线程继续");

        // Semaphore:同时只允许 2 个线程访问
        Semaphore sem = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    sem.acquire();                     // 获取许可(满了就阻塞)
                    System.out.println("线程 " + id + " 获得许可");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    sem.release();                     // 释放许可(必须 finally)
                }
            }).start();
        }
    }
}

6.7
ReentrantLock:可中断、可超时的锁

synchronized
拿到锁后不可中断、不可超时ReentrantLock
弥补了这些,还支持公平锁:

// LockDemo.java —— ReentrantLock 可超时获取锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();                                   // 线程1(主线程)先拿到锁
        new Thread(() -> {
            try {
                // 最多等 1 秒,拿不到就放弃(比 synchronized 灵活)
                if (lock.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("线程2 拿到锁");
                    } finally {
                        lock.unlock();
                    }
                } else {
                    System.out.println("线程2 等待超时,放弃");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
        lock.unlock();                                 // 线程1 释放
    }
}

6.8
双重检查锁单例:volatile 的经典应用

「懒加载单例」是面试必考。朴素的双重检查锁必须加
volatile
,否则指令重排可能导致别的线程拿到「半初始化」的对象:

// SingletonDemo.java —— 双重检查锁 + volatile(生产级写法)
public class SingletonDemo {

    // volatile 防止「分配内存->初始化->赋值引用」被重排成「分配内存->赋值引用->初始化」
    private static volatile SingletonDemo instance;

    private SingletonDemo() {}                         // 私有构造器,禁止外部 new

    public static SingletonDemo getInstance() {
        if (instance == null) {                        // 第一重检查:避免每次进来都加锁
            synchronized (SingletonDemo.class) {
                if (instance == null) {                // 第二重检查:避免重复创建
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

关键点:instance = new SingletonDemo()
不是原子操作(分配内存、初始化、赋引用三步),不加 volatile
时 JIT 可能重排,另一个线程在第一重检查处看到「非 null 但未初始化」的
instancevolatile
的内存屏障禁止了这种重排。

6.9
线程池执行流程(文字版对照)

把 6.3 的执行流程画成文字流程,配合 7 参数记忆:

新任务提交
  → 当前线程数 < corePoolSize ? 是 → 新建核心线程执行
  → 否 → 队列 workQueue 未满 ? 是 → 任务入队排队
  → 否 → 当前线程数 < maximumPoolSize ? 是 → 新建非核心线程执行
  → 否 → 走拒绝策略 handler

对应地理解 keepAliveTime
只对「非核心线程」生效:非核心线程空闲超过 keepAliveTime
就被回收,核心线程默认不回收(除非
allowCoreThreadTimeOut(true))。

6.10 死锁:如何产生与避免

两个线程各自持有对方需要的锁,又等待对方释放,就会死锁——程序卡死,线程永远阻塞。

// DeadlockDemo.java —— 演示死锁(用 jstack 可看到两个线程互相等待)
public class DeadlockDemo {
    private static final Object A = new Object();
    private static final Object B = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (A) {
                sleep(100);
                synchronized (B) { System.out.println("线程1 拿到 A 和 B"); }
            }
        }).start();
        new Thread(() -> {
            synchronized (B) {
                sleep(100);
                synchronized (A) { System.out.println("线程2 拿到 B 和 A"); }
            }
        }).start();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

避免死锁的三条原则:

  1. 固定加锁顺序:所有线程按相同顺序获取多把锁(上例改成都先
    A 后 B 就不死锁);
  2. 尽量用一把锁:能一把锁解决就别用两把;

  3. tryLock(超时)
    :拿不到就放弃,避免无限等待。

排查死锁:jstack <pid>
打印线程堆栈,底部会直接提示「Found one Java-level deadlock」。

本章小结

volatile 管可见性有序性、synchronized/CAS
管原子性;线程池必须显式 ThreadPoolExecutor
七参数、有界队列、命名线程、拒绝策略;ThreadLocal
traceId 必须 finally
remove——这三条是并发编程的生产铁律。

第 7 章 构建工具 Maven 实战

前面几章用 javac/java
是为了「零负担学语言」。进入真实项目,依赖管理、编译、打包、多模块,全部交给
Maven。本章讲清坐标、依赖传递与冲突、dependencyManagement、多模块和私服——这些是
Spring Boot 阶段每天都在用的基建。

7.1 坐标:依赖的唯一身份

Maven
用三个值唯一标识一个构件(artifact),称为坐标(GAV)

坐标项 含义 示例
groupId 组织/公司域名的反写 com.exampleorg.springframework.boot
artifactId 项目/模块名 user-servicespring-boot-starter-web
version 版本号 3.2.51.0.0-SNAPSHOT

一个标准的 pom.xml 最小骨架:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>user-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>       <!-- jar / war / pom(pom 用于父工程) -->

    <properties>
        <!-- 统一管理版本号与编码,子模块可继承 -->
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

7.2 依赖传递与冲突解决

Maven
会自动把「你依赖的构件所依赖的构件」也拉下来,这叫依赖传递。传递多了就会冲突(同一个构件出现多个版本)。解决冲突有三板斧:

  1. 最短路径优先:Maven
    默认选「依赖路径最短」的版本;
  2. 路径相同,先声明优先:谁先写进 pom.xml
    用谁;
  3. 显式控制(生产推荐):用
    dependencyManagement 统一版本 + exclusions
    排除传递依赖。

**scope(依赖范围)**也影响传递:compile(默认,会传递)、provided(编译期提供,运行期由容器提供,如
Servlet
API)、runtime(运行期才需要)、test(仅测试用)。

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.14.0</version>
    </dependency>

    <!-- 排除某个依赖里传递进来的、你不需要的构件(常见:排除旧版 slf4j 实现) -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>some-lib</artifactId>
        <version>1.0.0</version>
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

dependencyManagement
dependencies 的区别
(高频面试题):

  • dependencyManagement只声明版本、不实际引入。子模块要用时,只写
    groupId/artifactId,不写
    version
    ,版本自动继承。用于父工程统一锁版本。
  • dependencies实际引入依赖。

7.3
多模块项目(生产级完整示例)

生产项目通常按「分层」或「业务域」拆成多个 Maven
模块。下面是一个「父工程 + 两个子模块」的完整结构:

user-parent/                    # 父工程,packaging=pom
├── pom.xml                     # 定义 modules + dependencyManagement
├── user-common/                # 公共模块(放 User 实体、工具类)
│   └── pom.xml
└── user-service/               # 服务模块(依赖 user-common)
    └── pom.xml

父工程 user-parent/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>user-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>          <!-- 父工程必须是 pom 打包 -->

    <!-- 声明所有子模块,父工程构建时按顺序一起构建 -->
    <modules>
        <module>user-common</module>
        <module>user-service</module>
    </modules>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 统一锁版本:子模块引用时不用写 version -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>2.0.13</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.5.6</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

子模块 user-common/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 继承父工程:groupId/version 可省略,自动继承 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>user-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>user-common</artifactId>

    <dependencies>
        <!-- 版本由父工程 dependencyManagement 锁定,这里不写 version -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
    </dependencies>
</project>

子模块 user-service/pom.xml(依赖
user-common):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>user-parent</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>user-service</artifactId>

    <dependencies>
        <!-- 依赖本项目的 user-common 模块 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>user-common</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

user-parent 目录下构建:

mvn clean install        # clean 清理 + install 安装到本地仓库
# 构建顺序:先 user-common,再 user-service(因为 service 依赖 common)

关键点:多模块的构建顺序由模块间的依赖关系决定(Maven
自动拓扑排序),不依赖 <modules> 里的书写顺序。

7.4 私服与常用插件

私服(如 Nexus、Artifactory)是公司内部的「Maven
中央仓库代理 + 私有构件仓库」:

  • 作用一:缓存中央仓库,团队内网拉包快、稳定,不依赖外网;
  • 作用二:托管私有构件,团队内部模块(如
    user-common)通过私服共享;
  • 作用三:统一治理,控制依赖白名单、安全扫描。

上传到私服用 <distributionManagement> 配置:

<distributionManagement>
    <repository>
        <id>nexus-releases</id>
        <url>https://nexus.example.com/repository/maven-releases/</url>
    </repository>
    <snapshotRepository>
        <id>nexus-snapshots</id>
        <url>https://nexus.example.com/repository/maven-snapshots/</url>
    </snapshotRepository>
</distributionManagement>

注意:id 要和 ~/.m2/settings.xml 里配置的
<server>id 对应,私服的账号密码写在
settings.xml(不进代码库)。

常用插件(生产几乎必配):

插件 作用
maven-compiler-plugin 编译,指定 Java 版本(JDK 17)
maven-surefire-plugin 运行单元测试(mvn test 的底层)
maven-jar-plugin 打普通 jar 包
maven-shade-plugin 打「带所有依赖」的 fat jar

7.5 Maven 生命周期与常用命令

Maven 有三个内置生命周期,最常用的是
default(构建生命周期),它由一串**阶段(phase)**组成,按顺序执行(执行后面的阶段会自动先执行前面的):

validate → compile → test → package → verify → install → deploy

常用命令与对应阶段:

命令 作用
mvn clean 清理 target/ 目录
mvn compile 编译 src/main/java
mvn test 编译 + 跑单元测试(maven-surefire-plugin
mvn package 编译 + 测试 + 打包(jar/war)
mvn install package +
安装到本地仓库~/.m2/repository
mvn deploy install + 发布到私服/中央仓库

关键点:mvn install
是本地开发高频命令——把当前模块装进本地仓库,其他本地模块才能依赖到它。mvn deploy
才是「发布给团队」的动作。

7.6 settings.xml 与镜像加速

Maven 的全局配置
~/.m2/settings.xml(用户级)或
$MAVEN_HOME/conf/settings.xml(全局)。两件生产必配的事:

  1. 镜像(mirror):把中央仓库换成更快的内网私服或镜像源(如阿里云),加速依赖下载:
<settings>
    <mirrors>
        <mirror>
            <id>aliyun</id>
            <mirrorOf>central</mirrorOf>       <!-- 代理 central 中央仓库 -->
            <name>aliyun public</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </mirror>
    </mirrors>
</settings>
  1. 私服账号<servers>
    里配置私服的用户名密码,与 pom.xml
    <distributionManagement>id
    对应:
<settings>
    <servers>
        <server>
            <id>nexus-releases</id>
            <username>deploy-user</username>
            <password>xxxxxx</password>
        </server>
    </servers>
</settings>

关键点:账号密码放
settings.xml不进代码库),pom.xml
里只放 id 引用——这是「配置与代码分离」的体现。

本章小结

坐标(GAV)是依赖的身份,依赖传递会引发冲突(用
dependencyManagement + exclusions
治理),多模块用「父 POM(packaging=pom)+
<modules> +
继承」组织,私服负责内网缓存与私有构件共享——Maven 是后续所有 Spring Boot
项目的构建底座。

第 8 章 生产级实战与代码规范

最后一章回答一个问题:同样是能跑的代码,什么是「生产级」?
答案是规范。本章先讲阿里 Java 开发手册与 Clean Code
的核心条款,再给出「用户管理」项目的骨架设计,把前 7
章的知识点串起来。完整的可运行项目在第 5 节「生产级实战项目」给出。

8.1 命名规范(阿里手册)

命名是代码可读性的第一道关。阿里手册的命名铁律:

元素 规则 示例(正确 / 错误)
类名 UpperCamelCase(大驼峰) UserService / userservice
方法、变量 lowerCamelCase(小驼峰) findById / find_by_id
常量 UPPER_SNAKE_CASE MAX_RETRY_COUNT / maxRetryCount
包名 全小写 com.example.user
布尔变量 禁止 is 开头(POJO) deleted / isDeleted
数组 Type[](中缀) String[] args / String args[]
// 命名正误对照
public class NamingDemo {
    // 错误:常量应全大写下划线
    // public static final int maxRetryCount = 3;
    // 正确
    public static final int MAX_RETRY_COUNT = 3;

    public static void main(String[] args) {
        // 错误:魔法值 3 散落代码,语义不明
        // 正确:常量名自带语义
        int retry = MAX_RETRY_COUNT;
        System.out.println(retry);
    }
}

8.2
注释规范:解释「为什么」,不解释「是什么」

注释不是给代码「翻译一遍」,而是记录代码本身表达不了的决策、约束、坑

// 错误注释:复述代码(无信息量)
// 循环遍历用户列表
for (User user : users) { ... }

// 正确注释:解释为什么(有价值)
// 这里用 entrySet 而非 keySet,避免 get 的二次哈希开销(见第 2 章)
for (Map.Entry<Long, User> entry : userMap.entrySet()) { ... }

// 正确注释:标记已知坑
// 注意:toMap 的第三个参数是键冲突时的合并函数,不写会在 key 重复时抛 IllegalStateException
Map<Long, User> byId = users.stream()
    .collect(Collectors.toMap(User::getId, Function.identity(), (a, b) -> a));

Clean Code 的核心原则(选最关键的 4 条):

  1. 函数短小、单一职责:一个方法只做一件事,超过 20
    行就考虑拆;
  2. 命名即文档:好的变量名/方法名能替代一半注释;
  3. DRY(Don’t Repeat
    Yourself)
    :重复的代码抽成方法/工具类;
  4. 先写能跑的,再重构到干净:可运行是底线,干净是追求。

8.3 工具类与不可变对象

工具类(全是静态方法的类)必须私有构造器,防止被误实例化:

// 工具类正确写法:private 构造器 + final 类
public final class UserUtils {
    private UserUtils() {}                       // 禁止 new

    public static boolean isAdult(User user) {
        return user != null && user.getAge() != null && user.getAge() >= 18;
    }
}

不可变对象用 JDK 17 的
record(自动生成构造器、equals/hashCode/toString,且字段不可变):

// record:JDK 14+ 预览、17 正式可用。适合做 DTO、值对象
public record UserVO(Long id, String username, Integer age) {
    // 可在紧凑构造器里做校验(生产级:入参校验)
    public UserVO {
        if (id == null || username == null || username.isBlank()) {
            throw new IllegalArgumentException("id 和 username 不能为空");
        }
    }
}

关于 Lombok:生产项目普遍用 Lombok 的
@Data@Slf4j 来省掉
getter/setter/日志声明的样板代码。本阶段纯 Java
教学不引入
(先手写理解原理),从 Spring 阶段开始,全系列统一用
Lombok(构造器注入 @RequiredArgsConstructor +
final 字段、日志
@Slf4j)。这里先留个印象即可。

8.4 用户管理项目骨架设计

下面设计第 5 节完整项目的骨架:一个 User 实体 +
一个组合了 Stream 与线程池的 UserService。设计要点:

  1. User 实体:重写
    equals/hashCode(第 2 章);
  2. UserService:查询用 Stream 过滤/分组(第 4
    章),异步统计用线程池(第 6 章);
  3. 异常用 ErrorCode/BizException(第 5
    章)。
// User.java —— 用户实体(生产级:字段私有 + 重写 equals/hashCode)
import java.util.Objects;

public class User {
    private Long id;
    private String username;
    private Integer age;

    public User() {}                                   // 无参构造器(反射/Mapper 需要)

    public User(Long id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    // 业务主键是 id,equals/hashCode 只认 id
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', age=" + age + '}';
    }
}
// UserService.java —— 组合 Stream + 线程池 + 异常(骨架,完整版见第 5 节)
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class UserService {

    // 内存数据源:生产里这里是数据库/缓存,这里用 List 演示集合 + Stream
    private final List<User> users = new ArrayList<>();

    // 生产级线程池:有界队列 + 命名 + 拒绝策略(第 6 章)
    private final ThreadPoolExecutor pool = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        Runtime.getRuntime().availableProcessors() * 2,
        60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        new NamedThreadFactory("user-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

    /** 新增用户:先校验,再判重(用 Set 判重依赖 equals/hashCode */
    public void add(User user) {
        if (user == null || user.getUsername() == null || user.getUsername().isBlank()) {
            throw new BizException(ErrorCode.PARAM_ERROR);
        }
        if (users.contains(user)) {                 // contains 依赖 equals(只比 id)
            throw new BizException(ErrorCode.USER_EXISTS);
        }
        users.add(user);
    }

    /** 按年龄过滤(Stream filter */
    public List<User> listByAgeGreaterThan(int age) {
        return users.stream()
            .filter(u -> u.getAge() != null && u.getAge() > age)
            .collect(Collectors.toList());
    }

    /**  id  Map(防键重复) */
    public Map<Long, User> toMap() {
        return users.stream()
            .collect(Collectors.toMap(User::getId, Function.identity(), (a, b) -> a));
    }

    /** 按年龄分组(groupingBy */
    public Map<Integer, List<User>> groupByAge() {
        return users.stream()
            .filter(u -> u.getAge() != null)
            .collect(Collectors.groupingBy(User::getAge));
    }

    /** 异步统计平均年龄:用线程池执行(第 6 章) */
    public Future<Double> avgAgeAsync() {
        return pool.submit(() -> users.stream()
            .mapToInt(u -> u.getAge() == null ? 0 : u.getAge())
            .average()
            .orElse(0));
    }
}

关键点:这个骨架已经把本篇 80% 的知识点串起来了——实体重写
equals/hashCode、Stream 过滤/分组/转
Map、线程池异步、ErrorCode/BizException
异常、命名规范。第 5 节把它补全为「可运行的完整项目」。

8.5 更多阿里手册要点(高频)

除了 8.1 的命名,再补几条 Code Review 里最常见的:

  1. 判空用
    isEmpty()
    if (list.isEmpty()) 而非
    if (list.size() == 0)——语义更清晰。
  2. 常量/字面量在前"SUCCESS".equals(status)
    而非 status.equals("SUCCESS")——前者能防 status
    为 null 时的 NPE。
  3. POJO 的布尔字段不用 is
    开头
    private boolean deleted;isXxx
    会被框架当作 getter 名,产生歧义)。
  4. 参数校验放最前:入口处先校验再干活,if (param == null) throw ...,避免带着脏数据往下走。
  5. 避免多层嵌套if 深度不超过 3
    层,超过就抽方法或提前 return(卫语句)。
// 卫语句:提前 return,避免深层嵌套
public String getLevel(User user) {
    if (user == null) return "未知";             // 卫语句
    if (user.getAge() == null) return "未知";
    if (user.getAge() < 18) return "未成年";
    return "成年";                               // 主干逻辑清晰
}

8.6 单元测试基础:JUnit 5

生产级代码必须有测试。JUnit 5 是 Java 事实标准的测试框架(Spring
阶段会大量用到,这里先入门):

// UserServiceTest.java —— JUnit 5 单元测试(测试方法名描述场景)
import com.example.usermgmt.common.BizException;
import com.example.usermgmt.service.UserService;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Test
    void shouldThrowUserExistsWhenRegisterDuplicateId() {
        UserService service = new UserService();
        service.register(1L, "张三", 25);

        // 断言:重复 id 注册应抛 BizException,且错误码是 USER_EXISTS
        BizException e = assertThrows(BizException.class,
            () -> service.register(1L, "李四", 30));
        assertEquals(ErrorCode.USER_EXISTS.getCode(), e.getCode());
    }

    @Test
    void shouldThrowUserNotFoundWhenIdAbsent() {
        UserService service = new UserService();
        assertThrows(BizException.class, () -> service.findById(999L));
    }
}

三个测试铁律:一个测试只测一个行为测试方法名描述场景shouldXxxWhenYyy)、先写失败再写实现(TDD
的起点,后续阶段会展开)。

8.7 分层思想:为进入 Spring
做铺垫

第 5 节的项目已经在「分层」:repository(数据访问)→
service(业务)→ App(入口/表现)。后续 Spring
阶段会把这套分层映射为
controller / service / mapper / entity / dto / vo,并加上
ApiResult 统一响应体、GlobalExceptionHandler
全局异常处理。

这里先记住一句话:每层只做自己该做的事,层与层之间通过接口交互UserService
不关心数据存在哪(内存还是数据库),UserRepository
不关心业务规则——这种「面向接口、职责单一」的思维,是理解 Spring
依赖注入(IoC)的认知前提。

本章小结

生产级代码 = 正确的命名 + 解释「为什么」的注释 + 单一职责的小函数 +
私有构造器的工具类 + 不可变值对象;把这些规范应用到
User/UserService
上,就得到可扩展的项目骨架。


5. 生产级实战项目:用户管理

本节把前 8 章串成一个完整可运行的 Maven
项目
。它实现了「用户注册、查询、过滤、分组统计、异步统计、带过期时间的本地缓存」6
个功能,覆盖本篇 80% 以上的知识点:集合与
equals/hashCode、泛型与反射(缓存工具可扩展)、Stream、异常体系、线程池、Maven。

5.1 目录结构

user-mgmt/
├── pom.xml
└── src/main/java/com/example/usermgmt/
    ├── common/
    │   ├── ErrorCode.java
    │   └── BizException.java
    ├── entity/
    │   └── User.java
    ├── repository/
    │   └── UserRepository.java
    ├── service/
    │   └── UserService.java
    └── App.java

5.2 完整代码

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>user-mgmt</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

common/ErrorCode.java(与第 5
章逐字一致,后续阶段复用):

package com.example.usermgmt.common;

/** 统一错误码枚举:4xxxx 客户端可修复,5xxxx 服务端错误 */
public enum ErrorCode {
    SUCCESS(0, "success"),
    PARAM_ERROR(40001, "参数错误"),
    UNAUTHORIZED(40101, "未登录或登录已过期"),
    FORBIDDEN(40301, "无权限访问"),
    USER_NOT_FOUND(40401, "用户不存在"),
    USER_EXISTS(40002, "用户名已存在"),
    SYSTEM_ERROR(50000, "系统繁忙,请稍后重试"),
    ;

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
}

common/BizException.java

package com.example.usermgmt.common;

/** 统一业务异常:携带错误码,可保留原始异常链 */
public class BizException extends RuntimeException {

    private final int code;

    public BizException(ErrorCode ec) {
        super(ec.getMessage());
        this.code = ec.getCode();
    }

    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }

    public BizException(int code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public int getCode() { return code; }
}

entity/User.java

package com.example.usermgmt.entity;

import java.util.Objects;

/** 用户实体:业务主键 idequals/hashCode 只认 id */
public class User {
    private Long id;
    private String username;
    private Integer age;

    public User() {}

    public User(Long id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    @Override
    public String toString() {
        return "User{id=" + id + ", username='" + username + "', age=" + age + '}';
    }
}

repository/UserRepository.java(线程安全本地缓存,带过期时间,第
6 章 L3 实战):

package com.example.usermgmt.repository;

import com.example.usermgmt.entity.User;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 用户仓储:用 ConcurrentHashMap 实现线程安全的本地缓存。
 * 每个条目记录「数据 + 过期时间戳」,读取时惰性淘汰过期条目。
 * 生产里这里是数据库(MyBatis/JPA),本阶段用内存实现演示并发集合。
 */
public class UserRepository {

    /** 缓存条目:数据 + 过期时间(毫秒时间戳) */
    private static class CacheEntry {
        final User user;
        final long expireAt;   // 过期时刻(System.currentTimeMillis() + ttl)

        CacheEntry(User user, long expireAt) {
            this.user = user;
            this.expireAt = expireAt;
        }
    }

    // ConcurrentHashMap 保证并发读写安全,不需要手动加锁
    private final ConcurrentHashMap<Long, CacheEntry> cache = new ConcurrentHashMap<>();

    /** 写入:带过期时间(ttl 秒),过期后读取不到 */
    public void put(User user, long ttlSeconds) {
        long expireAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSeconds);
        cache.put(user.getId(), new CacheEntry(user, expireAt));
    }

    /** 读取:若已过期则惰性删除并返回 null,实现缓存自动过期 */
    public User get(Long id) {
        CacheEntry entry = cache.get(id);
        if (entry == null) {
            return null;
        }
        if (System.currentTimeMillis() > entry.expireAt) {
            cache.remove(id);          // 惰性淘汰:过期了顺手删掉
            return null;
        }
        return entry.user;
    }

    public void remove(Long id) {
        cache.remove(id);
    }

    public int size() {
        return cache.size();
    }

    /** 列出所有未过期用户(Stream 过滤过期条目) */
    public List<User> findAll() {
        long now = System.currentTimeMillis();
        return cache.values().stream()
            .filter(e -> now <= e.expireAt)      // 只返回未过期条目
            .map(e -> e.user)
            .collect(Collectors.toList());
    }
}

service/UserService.java

package com.example.usermgmt.service;

import com.example.usermgmt.common.BizException;
import com.example.usermgmt.common.ErrorCode;
import com.example.usermgmt.entity.User;
import com.example.usermgmt.repository.UserRepository;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;

/** 用户业务层:校验、去重、查询、分组统计、异步统计 */
public class UserService {

    private final UserRepository repository = new UserRepository();

    /** 生产级线程池:有界队列 + 命名 + 拒绝策略(第 6 章) */
    private final ThreadPoolExecutor pool = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors(),
        Runtime.getRuntime().availableProcessors() * 2,
        60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        new NamedThreadFactory("user-stat-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

    /** 注册用户:参数校验 + 判重,缓存 5 分钟 */
    public User register(Long id, String username, Integer age) {
        if (id == null || username == null || username.isBlank()) {
            throw new BizException(ErrorCode.PARAM_ERROR);
        }
        if (age != null && (age < 0 || age > 150)) {
            throw new BizException(ErrorCode.PARAM_ERROR.getCode(), "年龄不合法");
        }
        if (repository.get(id) != null) {
            throw new BizException(ErrorCode.USER_EXISTS);
        }
        User user = new User(id, username, age);
        repository.put(user, 300);   // 缓存 5 分钟
        return user;
    }

    /**  id 查询:查不到抛业务异常(Optional 风格) */
    public User findById(Long id) {
        return Optional.ofNullable(repository.get(id))
            .orElseThrow(() -> new BizException(ErrorCode.USER_NOT_FOUND));
    }

    /** 列出所有用户(缓存 size 有限,演示用) */
    public List<User> listAll() {
        return List.copyOf(repositoryCacheValues());
    }

    /** 年龄过滤(Stream filter */
    public List<User> listAdults(int minAge) {
        return repositoryCacheValues().stream()
            .filter(u -> u.getAge() != null && u.getAge() >= minAge)
            .collect(Collectors.toList());
    }

    /** 按年龄分组(Stream groupingBy */
    public Map<Integer, List<User>> groupByAge() {
        return repositoryCacheValues().stream()
            .filter(u -> u.getAge() != null)
            .collect(Collectors.groupingBy(User::getAge));
    }

    /**  MaptoMap 带合并函数防键重复) */
    public Map<Long, User> toIdMap() {
        return repositoryCacheValues().stream()
            .collect(Collectors.toMap(User::getId, Function.identity(), (a, b) -> a));
    }

    /** 异步统计平均年龄(线程池执行,Future 拿结果) */
    public Future<Double> averageAgeAsync() {
        return pool.submit(() -> repositoryCacheValues().stream()
            .mapToInt(u -> u.getAge() == null ? 0 : u.getAge())
            .average()
            .orElse(0));
    }

    /** 仓库当前数据(从缓存里捞出所有未过期 User */
    private List<User> repositoryCacheValues() {
        return repository.findAll();
    }

    /** 关闭线程池(生产停机必须优雅关闭) */
    public void shutdown() {
        pool.shutdown();
    }

    /** 复用第 6 章的自定义命名线程工厂 */
    static class NamedThreadFactory implements ThreadFactory {
        private final String prefix;
        private final AtomicLong seq = new AtomicLong(0);
        NamedThreadFactory(String prefix) { this.prefix = prefix; }
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, prefix + "-" + seq.incrementAndGet());
        }
    }
}

App.java(程序入口,演示全部功能):

package com.example.usermgmt;

import com.example.usermgmt.common.BizException;
import com.example.usermgmt.entity.User;
import com.example.usermgmt.service.UserService;

import java.util.concurrent.Future;

/** 用户管理程序入口:演示注册、查询、过滤、分组、异步统计、异常处理 */
public class App {

    public static void main(String[] args) throws Exception {
        UserService service = new UserService();

        // 1. 注册用户
        service.register(1L, "张三", 25);
        service.register(2L, "李四", 30);
        service.register(3L, "王五", 25);
        service.register(4L, "赵六", 18);
        System.out.println("=== 注册完成 ===");

        // 2. 判重:重复 id 抛 USER_EXISTS
        try {
            service.register(1L, "张三重复", 99);
        } catch (BizException e) {
            System.out.println("判重生效: code=" + e.getCode() + ", msg=" + e.getMessage());
        }

        // 3. 查询:查不到抛 USER_NOT_FOUND
        try {
            service.findById(999L);
        } catch (BizException e) {
            System.out.println("查询生效: code=" + e.getCode() + ", msg=" + e.getMessage());
        }

        // 4. Stream 过滤 + 分组 + 转 Map
        System.out.println("成年人(>=18): " + service.listAdults(18));
        System.out.println("按年龄分组: " + service.groupByAge());
        System.out.println("按 id 转 Map: " + service.toIdMap());

        // 5. 异步统计(线程池)
        Future<Double> avgFuture = service.averageAgeAsync();
        System.out.println("平均年龄(异步): " + avgFuture.get());

        // 6. 优雅关闭
        service.shutdown();
    }
}

5.3 运行步骤

# 1. 按上面目录结构建好文件后,在 user-mgmt 目录下执行:
cd user-mgmt
mvn clean compile        # 编译(若首次运行会自动下载 Maven 插件)

# 2. 运行主类(maven exec 插件,或手动 classpath)
mvn exec:java -Dexec.mainClass="com.example.usermgmt.App"

# 备选:不用 exec 插件,手动运行
# mvn package
# java -cp target/classes com.example.usermgmt.App

期望输出(顺序略有差异属正常):

=== 注册完成 ===
判重生效: code=40002, msg=用户名已存在
查询生效: code=40401, msg=用户不存在
成年人(>=18): [User{id=1, username='张三', age=25}, User{id=2, username='李四', age=30}, User{id=3, username='王五', age=25}, User{id=4, username='赵六', age=18}]
按年龄分组: {18=[User{id=4, username='赵六', age=18}], 25=[User{id=1, username='张三', age=25}, User{id=3, username='王五', age=25}], 30=[User{id=2, username='李四', age=30}]}
按 id 转 Map: {1=User{...}, 2=User{...}, 3=User{...}, 4=User{...}}
平均年龄(异步): 24.5

说明:如果你没有 exec-maven-plugin,运行
mvn exec:java 时 Maven
会自动去中央仓库下载该插件(需要联网)。也可以像前面几章一样,直接用
javac 编译 src/main/java 下的所有
.java 再运行,二者等价。


6. 常见坑与排错指南

坑 / 现象 原因 解决方案
ConcurrentModificationException for-each 遍历时直接 list.remove(),触发 fail-fast removeIfIterator.remove()
Set/Map 去重或查找「时灵时不灵」 实体只重写 equals 没重写 hashCode 二者必须同时重写,且用相同字段
遍历 Map 慢、被 Code Review 打回 keySet()get(),二次哈希 entrySet() 一次拿 key 和 value
toMap
IllegalStateException: Duplicate key
key 重复且没传合并函数 传第三个参数 (a, b) -> a
(a, b) -> b
金额计算少几分钱、对不上账 double/float 二进制误差,或
new BigDecimal(0.1)
一律 BigDecimal,字符串/valueOf
构造,compareTo 比较
循环里字符串拼接内存暴涨 String 不可变,+ 产生大量中间对象 StringBuilder
线程池任务堆积 → OOM Executors.newFixedThreadPool() 用无界队列 ThreadPoolExecutor + ArrayBlockingQueue
有界队列 + 拒绝策略
方法里到处 new Thread(),线程失控 无统一管理 统一走线程池,禁止裸 new Thread()
线上日志串了用户(A 的 traceId 出现在 B 请求) ThreadLocal
remove,线程池复用线程导致脏数据
finallyThreadLocal.remove()
异常被吞、线上无日志无堆栈 空 catch / e.printStackTrace() / catch
return null
异常上抛统一处理,log.error("...", e) 带堆栈
volatile 计数结果不对 volatile 只保证可见性,不保证 i++
原子性
AtomicIntegersynchronized
Maven 依赖版本冲突、拉错包 依赖传递多个版本,最短路径优先命中旧版 dependencyManagement 锁版本 + exclusions
排除
反射 newInstance
NoSuchMethodException
类没有无参构造器 提供无参构造器,或 getDeclaredConstructor(参数)
匹配

8. 总结与延伸阅读

总结

本阶段你建立的是「生产级 Java」的完整地基:从环境与调试,到集合底层与
equals/hashCode 契约,到泛型/注解/反射,到
Stream/Optional
的简洁写法,到异常与日志体系,到线程池与
ThreadLocal,最后用 Maven
多模块和「用户管理」项目把它们串成可运行的整体。记住三个贯穿全篇的「生产级心智」:凡事显式(错误码显式、空值显式、线程池参数显式)、凡事有界(队列有界、线程有界)、凡事可观测(命名线程、结构化日志、traceId)。这些能力会被后续
11 个 Spring Boot 阶段逐层放大,但地基在这里。

延伸阅读

  1. 官方文档(优先):《The Java™ Tutorials》—— Collections
    Concurrency
    章节;
  2. 《Effective Java》(第 3 版)—— 生产级 Java
    的圣经,重点看「equals/hashCode」「泛型」「Lambda 与
    Stream」「并发」几章;
  3. 《Java 并发编程实战》——
    系统理解可见性/原子性/有序性与线程池;
  4. 《深入理解 Java 虚拟机》(第 3 版)—— 想追到 JVM
    内存、GC、锁升级的底层必读;
  5. 《阿里巴巴 Java 开发手册》——
    国内事实标准的编码规范,配套 IDEA 插件可实时检查。

本阶段完 · 下一篇《阶段 1:Spring 基础与
IoC/DI》将用你在本篇打下的 Java 地基,正式进入 Spring 世界。

正文完
 0
评论(没有评论)