# 《编写高质量代码:改善Java程序的151个建议》知识点全解 > AI生成|2026年10月28日|codebuddy|原书:秦小波《编写高质量代码:改善Java程序的151个建议》,机械工业出版社,2012-01-01 > **我的阅读**:进度 100%|累计 6小时26分钟|开始 2017-11-21|最近 2021-04-27|读完 2018-02-02 ## 一句话总结 这本书把 Java 里那些「语法上能跑、工程上会出事」的细节,拆成 151 条可执行的编码建议:从类型与边界,到集合与并发,再到工程判断力,目标是把代码从「能编译」推到「可维护、可协作、少踩坑」。 ## 适合谁 / 前置知识 **适合**:写过半年以上 Java、能独立完成一个模块的开发者;正在从「功能实现者」转向「代码负责人」的人;需要做 Code Review、定团队规范的技术骨干;准备面试、想系统补齐语言细节的人。 **不适合**:零基础读者。书中大量条目以「你知道这样写会错」为前提,没有 Java 语法、面向对象、集合框架、线程基础的人读起来会变成背结论。也不适合寻找框架实战(Spring、分布式)的人——本书几乎不涉及框架应用。 **前置知识**:Java 基础语法与面向对象;集合框架(List/Map/Set)的基本用法;异常体系;线程的创建与 synchronized 的基本概念;最好动手写过被坑过的代码,否则条目记不住。 ## 一、全书地图 全书 12 章、151 条建议,整体是「语言细节 → 并发与性能 → 工程与思想」的三层递进。前七章讲**单个语句、单个类写得对不对**;第八、九章讲**出错与并发时系统的行为**;第十至十二章讲**选型、效率与判断力**。章节号为原书目录章节,建议编号未在目录中体现,故不标注。 | 章 | 主题 | 解决的核心问题 | 在全书中的位置 | |---|---|---|---| | 第1章 | Java 开发中通用的方法和准则 | 运算符、类型转换、常量、边界值等跨场景通则 | 总纲,后面各章反复引用 | | 第2章 | 基本类型 | 精度、溢出、包装类、随机数 | 第1章的类型细化 | | 第3章 | 类、对象及方法 | 构造器、equals/hashCode、clone、静态与实例 | 从「语句」上升到「类」 | | 第4章 | 字符串 | 不可变性、拼接、编码、比较 | 最高频的日常陷阱 | | 第5章 | 数组和集合 | 视图、遍历删除、排序、Map 使用 | 与第3章契约强相关 | | 第6章 | 枚举和注解 | 常量建模、单例、元数据 | 用类型系统替代魔法值 | | 第7章 | 泛型和反射 | 类型擦除、协变、运行时类型 | 连接编译期与运行期 | | 第8章 | 异常 | 流程控制、资源释放、异常传播 | 系统健壮性 | | 第9章 | 多线程和并发 | 锁、可见性、线程池、ThreadLocal | 系统正确性的深水区 | | 第10章 | 性能和效率 | 对象创建、装箱、字符串、缓存 | 在正确之上谈成本 | | 第11章 | 开源世界 | 选轮子、读源码、依赖与协议 | 从写代码到用生态 | | 第12章 | 思想为源 | DRY、KISS、YAGNI、重构与命名 | 把规则升维成判断力 | **逻辑关系**:第1–2章是「值」的正确性;第3–7章是「类型与结构」的正确性;第8–9章是「运行时」的正确性;第10章是「成本」;第11–12章是「决策」。前九章回答「这样写会不会错」,后三章回答「这样写值不值」。 ## 二、核心知识点 ### 1. 奇偶判断与整数边界 **是什么**:判断奇偶应写 `i % 2 == 0`(偶数),而不是 `i % 2 == 1`(奇数)。Java 取余结果与被除数同号,`-3 % 2 == -1`,用 `== 1` 判断负数奇数会失败。 **为什么重要**:这类错误不会编译报错,只在特定输入下暴露,测试用例往往只覆盖正数。 **怎么用**:优先用位运算或偶数判断;涉及范围计算时,先想清楚 `Integer.MAX_VALUE + 1` 会回绕成负数,`Math.abs(Integer.MIN_VALUE)` 仍是负数。 **常见误区**:以为「加法不会溢出」「取绝对值一定为正」;用 `int` 存毫秒级时间戳的差值而不评估量级。 ### 2. 货币与小数必须用 BigDecimal **是什么**:`float`/`double` 是二进制浮点,无法精确表示 0.1,`0.1 + 0.2 != 0.3`。金额、费率、对账必须用 `BigDecimal`。 **为什么重要**:一分钱的误差在批量结算、反复累加后会被放大,属于业务事故而非代码瑕疵。 **怎么用**:用字符串构造 `new BigDecimal("0.1")` 或 `BigDecimal.valueOf(0.1)`;除法必须指定精度和舍入模式,否则除不尽会抛 `ArithmeticException`;比较数值用 `compareTo`,因为 `equals` 连 scale 一起比,`1.0` 与 `1.00` 不相等。 **常见误区**:用 `new BigDecimal(0.1)`(把误差原样带入);用 `==` 比较 BigDecimal;四舍五入一律用 `HALF_UP` 而不确认财务规则。 ### 3. 包装类型的 null、缓存与相等 **是什么**:包装类是对象,可能为 `null`;`==` 比较引用而非数值;`Integer` 默认缓存 -128~127,超出范围每次装箱都是新对象。 **为什么重要**:`Integer a = 127, b = 127; a == b` 为 true,换成 128 就为 false——同一份代码在不同数据下结果不同,是最难复现的 bug 之一。 **怎么用**:包装类型比较大小时用 `equals` 或先拆箱;方法返回值允许缺失时才用包装类,参与运算前判空;集合中存对象用包装类,局部变量和循环计数器优先基本类型。 **常见误区**:认为自动装箱「没有成本」;在 `for` 循环里对 `Long` 做累加,制造大量临时对象;三元表达式两分支分别为包装类和基本类型时,发生隐式拆箱导致 NPE。 ### 4. equals 与 hashCode 的契约 **是什么**:重写 `equals` 必须重写 `hashCode`;相等的对象必须有相同的哈希值,反之不要求。 **为什么重要**:`HashMap`、`HashSet` 先用哈希定位桶,再用 `equals` 比较。只重写 `equals` 会让「相等的对象」被存进不同桶,出现放进去却取不出来、Set 出现重复元素。 **怎么用**:用业务主键或全部关键字段实现两者;`equals` 先判 `==`、再判类型、再比字段;继承场景慎用 `getClass` 与 `instanceof`,明确是否需要子类相等。 **常见误区**:用可变字段参与 `hashCode`,对象入集合后修改字段导致「失联」;`equals` 参数写成具体类型而非 `Object`,实际是重载不是重写。 ### 5. 构造器、工具类与拷贝 **是什么**:工具类应私有化构造器并加注释,防止被实例化和继承;构造器里不要调用可被覆写的方法;`clone` 默认是浅拷贝。 **为什么重要**:工具类被 `new` 出来不会报错,但语义混乱;构造器中调用可覆写方法,子类字段尚未初始化就被使用;浅拷贝会让两个对象共享同一个内部集合,改一个动两个。 **怎么用**:工具类用 `final` + 私有构造;对象拷贝优先拷贝构造器或静态工厂,明确写出深拷贝逻辑;确实要 `clone` 时实现 `Cloneable` 并逐层处理引用字段。 **常见误区**:以为实现了 `Cloneable` 就自动深拷贝;在构造器里注册监听器、启动线程、调用外部服务,导致对象「半成品」外泄。 ### 6. 字符串:直接量、拼接与比较 **是什么**:`String` 不可变;字面量进入常量池,`new String(...)` 一定创建新对象;循环内 `+=` 会不断产生中间对象。 **为什么重要**:字符串是使用频率最高的类型,错误用法同时带来正确性问题和 GC 压力。 **怎么用**:比较内容一律 `equals`,常量放前面避免 NPE;循环拼接用 `StringBuilder`(单线程)或 `StringBuffer`;单行简单拼接编译器会优化,不必为 `"a" + b` 强行改写;`intern` 只在确有大量重复字符串、且确认内存收益时使用。 **常见误区**:用 `==` 比较从外部读入的字符串;在循环里用 `+` 拼 SQL 或日志;认为 `StringBuilder` 一定比 `+` 快,忽略可读性成本。 ### 7. asList 与 subList 返回的是「视图」 **是什么**:`Arrays.asList` 返回的是数组支撑的固定长度列表,`add`/`remove` 抛 `UnsupportedOperationException`;`List.subList` 返回原列表的视图,不是独立副本。 **为什么重要**:两者签名都返回 `List`,调用方按普通 `ArrayList` 使用,运行时才炸;`subList` 还会因原列表结构改变而抛 `ConcurrentModificationException`。 **怎么用**:需要可变列表时 `new ArrayList<>(Arrays.asList(...))`;`asList` 适合「只读遍历」和「把数组当参数传」;`subList` 用于分段处理后立即消费,需要保留就复制出来。 **常见误区**:用 `asList` 包裹基本类型数组(`int[]` 会得到一个只有 1 个元素的 `List`);把 `subList` 结果长期持有并同时修改原列表。 ### 8. 遍历中删除元素与 remove 重载 **是什么**:`for-each` 遍历时用集合自身 `remove` 会抛 `ConcurrentModificationException`(单线程下由 fail-fast 计数器触发);`List.remove(int)` 删下标,`remove(Object)` 删元素。 **为什么重要**:前者是最高频的运行时异常之一;后者在 `List` 上极易误删——`remove(1)` 删的是下标 1,不是值为 1 的元素。 **怎么用**:删除用 `Iterator.remove()`,或 Java 8+ 的 `removeIf`;并发场景用 `ConcurrentHashMap`、`CopyOnWriteArrayList` 等并发容器;删除包装类型元素时显式传 `Integer.valueOf(x)` 或先取下标。 **常见误区**:以为 `for-each` 里 `break` 前删一个没事(有些实现确实不抛,属于运气);用 `size()` 做循环上界同时删除元素导致漏删。 ### 9. Map 的遍历、容量与 key **是什么**:遍历 `HashMap` 应使用 `entrySet` 而非 `keySet` 再 `get`;HashMap 有容量与负载因子,扩容要 rehash;key 必须稳定可变性。 **为什么重要**:`keySet + get` 是两次查找,数据量大时是明显的常数浪费;key 用可变对象会让哈希值变化,entry 永远找不到。 **怎么用**:已知数据量时预设容量,避免多次扩容;遍历用 `entrySet`,只需 key 时用 `keySet`;key 选不可变类型(String、枚举、包装类),自定义对象必须正确实现 `equals/hashCode` 且入 Map 后不再修改参与哈希的字段。 **常见误区**:用 `HashMap` 存需要排序的结果(应用 `TreeMap` 或排序后输出);认为 `HashMap` 线程安全;在并发下用 `HashMap` 导致 JDK 7 链表成环(JDK 8 已改进但仍不安全)。 ### 10. 用枚举替代 int 常量和「伪单例」 **是什么**:枚举是类型安全的常量集合,天然限制取值范围;单元素枚举是实现单例最简洁、且能防反射与反序列化破坏的方式。 **为什么重要**:`public static final int STATUS_OK = 1` 无法阻止传入 99;枚举让非法值在编译期或反序列化时就被拒绝,`switch` 也能获得穷举提示。 **怎么用**:状态、类型、错误码用枚举;枚举可带字段和方法承载行为;需要单例时优先 `enum Singleton { INSTANCE; }`;`values()` 每次返回新数组,频繁调用可缓存。 **常见误区**:用 `ordinal()` 做持久化(枚举顺序一变数据全错,应存 `name` 或自定义 code);认为枚举不能有状态(可以有,但必须是不可变设计)。 ### 11. 泛型擦除与「不协变」 **是什么**:泛型在编译后被擦除,`List` 与 `List` 运行时是同一个类;泛型不协变,`List` 不是 `List` 的子类型;数组是协变的。 **为什么重要**:这解释了为什么不能 `new T[]`、不能 `instanceof List`、为什么把 `List` 赋给 `List` 会编译失败——若允许,就能往字符串列表里塞整数。 **怎么用**:用通配符表达输入输出:读取用 `? extends T`,写入用 `? super T`(PECS);需要运行时类型信息时显式传 `Class`;不要使用原始类型 `List`。 **常见误区**:以为运行时能拿到泛型实参;用反射往 `List` 里塞 `Integer` 后才发现「泛型是编译期的幻觉」;把数组的协变习惯套到集合上。 ### 12. 异常的四个硬规则 **是什么**:不用异常控制正常流程;`finally` 中不 `return`、不抛异常;捕获后要么处理要么包装并保留 `cause`;资源用 try-with-resources 释放。 **为什么重要**:`finally` 里的 `return` 会吞掉正在传播的异常,让故障「消失」;丢掉 `cause` 会让堆栈断链,线上排查失去根因;异常做流程控制会让正常路径也背上堆栈开销。 **怎么用**:能预判的条件用 `if` 判断,异常只表达「意外」;包装时 `new BizException("...", e)` 保留原因;自定义异常按「调用方能否恢复」划分受检/非受检;关闭资源用 try-with-resources,避免手写 `finally` 出错。 **常见误区**:`catch (Exception e) {}` 空捕获;`catch` 后只打一行 `e.getMessage()` 丢掉堆栈;在 `finally` 里关闭多个资源,前一个抛异常导致后一个不关闭。 ### 13. 并发的三个基本事实 **是什么**:`synchronized` 锁的是对象不是代码块;`volatile` 保证可见性和有序性,不保证原子性;线程是昂贵资源,应使用线程池。 **为什么重要**:锁错对象等于没加锁(如锁 `String` 常量、锁被复用的缓存对象);`volatile int i; i++` 依然丢失更新;每请求 `new Thread` 在流量上来时直接拖垮系统。 **怎么用**:锁用 `private final` 专用对象;计数用 `AtomicInteger` 或 `LongAdder`;线程池显式指定核心数、队列、拒绝策略和线程名;`ThreadLocal` 用完必须 `remove()`,否则线程池复用线程时造成内存泄漏和脏数据。 **常见误区**:认为 `volatile` 能替代锁;在 `wait/notify` 外层漏写 `while` 循环导致虚假唤醒;用 `Thread.stop` 终止线程(已废弃,应使用中断标志)。 ### 14. 性能:先看成本在哪里 **是什么**:多数性能问题不是算法,而是无意识的重复创建、装箱、字符串拼接和无效日志。 **为什么重要**:这些开销单次可忽略,但在循环、批量、高 QPS 下会线性放大,且往往在压测时才暴露。 **怎么用**:循环内避免重复创建对象和不变量计算;警惕 `Long`/`Integer` 在循环中的自动装箱;日志先判断级别再拼字符串(`if (log.isDebugEnabled())`);集合预设容量;热点路径再谈优化,先用数据定位。 **常见误区**:不做测量就「优化」,把可读代码改成难维护的位运算;为了性能滥用静态缓存,引入一致性和内存问题;把「少 new 对象」当成教条,牺牲正确性。 ### 15. 工程判断力:轮子、源码与取舍 **是什么**:不重复造轮子,但要能判断轮子是否合适;读源码是理解框架边界的最快方式;设计上遵循 DRY、KISS、YAGNI。 **为什么重要**:多数线上问题来自「自己写了一个不完整的通用组件」和「用了不理解的框架」;过度设计和复制粘贴是两种相反的浪费。 **怎么用**:选型看维护活跃度、社区、协议、与现有栈的匹配度;引入依赖前查冲突与传递依赖;遇到「约定俗成」的写法去源码里找依据;抽象等到第三次重复再提取,而不是第一次就设计框架。 **常见误区**:把「能跑」当「可维护」;为了炫技引入复杂设计模式;盲目追新版本而忽略团队与生产环境约束。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | 自动装箱/拆箱 | 基本类型与包装类的隐式转换,可能创建对象或抛 NPE | 集合存值、方法返回值、三元表达式 | | 整数缓存 | `Integer` 默认缓存 -128~127,超出即新对象 | 用 `==` 比较包装类时的隐形坑 | | 类型擦除 | 泛型信息编译后消失,运行时无实参类型 | 反射、数组创建、`instanceof` | | PECS | Producer Extends, Consumer Super | 设计泛型 API 的通配符方向 | | fail-fast | 集合结构被并发修改时立即抛异常 | 遍历删除、多线程共享集合 | | 视图(view) | 返回的是原数据的窗口而非副本 | `asList`、`subList`、`keySet` | | 幂等 | 同一操作执行多次结果一致 | 接口设计、重试、消息消费 | | 可见性 | 一个线程的写入何时对另一线程可见 | `volatile`、`synchronized`、并发容器 | | 原子性 | 操作不可被中断地整体完成 | `i++`、`AtomicInteger`、锁 | | 浅拷贝/深拷贝 | 复制引用还是递归复制对象图 | `clone`、DTO 转换、缓存 | | 受检异常 | 编译器强制处理的异常 | API 设计、调用方契约 | | DRY / KISS / YAGNI | 不重复、保持简单、别提前实现 | 重构与需求取舍 | ## 四、可复用的方法/框架 **写一段代码前的四问** 1. **边界**:输入为 0、负数、`null`、最大值时会怎样? 2. **类型**:这里该用基本类型还是包装类?精度够不够?会不会溢出? 3. **契约**:我重写了 `equals` 吗?`hashCode` 跟上了吗?返回的是副本还是视图? 4. **成本**:这段在循环里吗?有没有重复创建、重复查询、重复拼接? **Code Review 的五层扫描** 1. **正确性层**:边界、空值、异常路径、并发共享状态。 2. **契约层**:`equals/hashCode`、`compareTo`、序列化、可见性修饰。 3. **资源层**:流、连接、线程、`ThreadLocal` 是否在所有路径释放。 4. **可读层**:命名是否表达意图、方法是否只做一件事、注释是否解释「为什么」。 5. **取舍层**:这处抽象是否必要、这个依赖是否值得、这段优化是否有数据支撑。 **遇到「诡异 bug」的排查顺序** 先怀疑自己的类型与边界(溢出、装箱、精度)→ 再怀疑集合契约(哈希、视图、并发修改)→ 再怀疑并发(可见性、锁对象)→ 最后怀疑框架与依赖版本。多数「玄学问题」在前两层就能定位。 **选型判断清单** 维护是否活跃 → 文档与社区 → 许可证是否兼容 → 与现有依赖是否冲突 → 出问题能否读源码定位 → 团队是否有人能维护。六项中若有硬伤,宁可不用。 ## 五、我的阅读轨迹 | 项目 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 6小时26分钟 | | 开始阅读 | 2017-11-21 | | 最近阅读 | 2021-04-27 | | 读完时间 | 2018-02-02 | **复习建议**:全书已读完,但读完时间(2018-02)与最近阅读(2021-04)相隔三年,说明这是一本「查阅型」而非「通读型」的书。6 小时 26 分钟对应的是一次偏快的通读,条目记忆衰减明显。 - **第一优先**:第5章(数组和集合)、第9章(多线程和并发)。这两章篇幅最大、坑最深,也是最容易在实际代码里复现的。 - **第二优先**:第2章(基本类型)、第4章(字符串)、第8章(异常)。属于每天都在用、错了就出线上问题的部分。 - **按需查阅**:第7章(泛型和反射)、第10章(性能和效率),在写通用组件或做压测时回看。 - **建议方式**:不要重读一遍。把本文第二部分的 15 个知识点当作检查表,每次 Code Review 挑 2–3 条对照实际代码,比通读记忆效率高得多。 ## 六、行动清单 - 为当前项目建一份《编码红线》清单,先收录本文的 15 条,落到 PR 模板里。 - 用静态检查(SpotBugs / Error Prone / Sonar)把 `==` 比较包装类、空 `catch`、`finally` 中 `return` 设为阻断项。 - 审查现有代码中所有金额字段,确认是否全链路使用 `BigDecimal` 且指定了舍入模式。 - 排查所有 `Arrays.asList` 与 `subList` 的调用点,确认没有对其进行增删或长期持有。 - 检查线程池配置:是否有界队列、是否有拒绝策略、线程是否有命名、`ThreadLocal` 是否 `remove`。 - 把循环内的字符串拼接、日志拼接、重复 `get` 改为 `StringBuilder`、级别判断、`entrySet`。 - 选一个在用框架,读它一个核心类的源码,写下三条「它的边界在哪里」。 - 每月复盘一次线上问题,标注它属于本文哪一条知识点的反面,形成自己的错题本。 ## 七、延伸阅读 1. **《Effective Java》(Joshua Bloch)**——本书的思想源头之一,条目式写法更凝练、更贴近语言规范。建议**先读本书建立场景感,再读《Effective Java》建立原则体系**,后者偏重「为什么这样设计」,需要一定经验才能消化。 2. **《Java 并发编程实战》(Brian Goetz 等)**——本书第9章只给了结论,这本给的是模型:内存模型、可见性、锁的代价、并发容器的内部结构。建议在读完本书第9章后紧接着读,顺序不要颠倒。 3. **《重构:改善既有代码的设计》(Martin Fowler)**——本书第12章谈「思想」,这本谈「手法」:如何在不改变行为的前提下小步改造。建议放在最后,先用前两本建立「什么是对的」,再用这本建立「怎么改过去」。 **阅读顺序建议**:本书(建立场景与清单)→ 《Effective Java》(建立原则)→ 《Java 并发编程实战》(补齐并发模型)→ 《重构》(落地改造手法)。