TECH NOTES / Java 与 Spring

Java 代码实践:35 条建议的重新整理

原笔记 2021-10-12 · 整理于 2026-10-05

原笔记叫“提高千倍效率的 Java 代码小技巧”。重新读一遍,有些建议是在讲代码习惯,有些是在讲资源管理,还有一些性能结论缺少成立条件。

这次保留原来的 35 个话题,重新整理说明和示例。这里不承诺某个写法必然快多少:先保证结果正确,再定位瓶颈,最后用数据判断优化有没有价值。

一、变量、对象与集合

  1. final 表达约束。 用它说明引用不再赋值、方法不可重写或类不可继承,不要据此推导“性能固定提高 50%”。
  2. 对象复用要看场景。 在循环里逐步拼接大量字符串时可考虑 StringBuilder;不要为了省一个对象引入难以维护的共享状态。
  3. 缩小变量作用域。 局部变量有助于理解代码。不能仅凭变量写在方法内,就断言它引用的对象不需要垃圾回收。
  4. 及时关闭外部资源。 文件、连接与流需要按生命周期释放,不要把资源句柄管理交给 GC。
  5. 缓存计算结果要保证语义不变。 循环期间集合可能变化时,缓存 size() 会改变遍历范围;ArrayList.size() 本身是常数时间操作。
  6. 按需创建。 昂贵且不一定用到的对象可以延迟创建,但要考虑并发初始化和首次访问的延迟。
  7. 异常用于表达失败。 不要靠不断抛出异常完成正常分支判断;也不要为了少抛异常而吞掉真正的失败。
  8. try/catch 放哪里取决于业务。 一条失败要不要终止整个批次,决定了它应在循环内部还是外部,不能机械地挪动。
  9. 容量预估需要合理。 ArrayList、HashMap 和 StringBuilder 的容量不是同一个概念,LinkedList 也没有初始容量参数。预估过大同样浪费内存。
  10. 数组复制优先使用现成 API。 根据是否需要新数组,考虑 System.arraycopy 或 Arrays.copyOf,先检查边界和类型。
  11. 不要机械地用移位替代除法。 例如负数的整数除法与算术右移可能得到不同结果,读得懂且语义正确更重要。
  12. 把引用移到循环外不等于少创建对象。 循环里仍然执行 new,创建次数就没有因此减少,还可能扩大变量作用域。
  13. 数组与集合按接口需求选择。 固定大小、底层数据处理可以使用数组;需要增删与集合操作时使用合适的集合。
  14. 线程安全不能只看类名。 单线程使用 HashMap、ArrayList 很常见;并发环境要按访问方式设计同步或并发集合。
  15. final 数组仍可修改元素。 对外暴露可变数组时需要考虑防御性复制,而不是只给引用加 final。
  16. 单例也有生命周期和并发问题。 只在确实需要共享的场景使用,单例本身不保证其字段线程安全。
  17. 静态引用要有明确用途。 长期持有无用对象可能阻止回收;缓存应有容量、淘汰和清理策略。
  18. 会话按业务失效。 注销或权限变更时处理会话,超时配置结合实际系统,不能把某个容器默认值当成统一标准。
  19. 遍历方式结合数据结构。 索引遍历链表可能反复查找节点;普通业务先选清晰写法,热点路径再测量。ArrayList 与 LinkedList 的官方说明可以用来核对操作成本。

二、并发、接口与可读性

  1. 同步范围围绕共享不变量。 缩小锁范围的前提是不会破坏一致性,不能单纯为了“锁得少”拆开需要原子完成的操作。
  2. 常量有明确类型与语义。 static final 很适合常量声明,但并非所有这类字段都是编译期常量。
  3. 清理无用代码。 删除不使用的局部变量、导入和死分支,主要收益是降低阅读和维护成本。
  4. 反射按需求使用。 框架接入与动态扩展常常需要反射;发现热点后再考虑缓存元数据或调整调用方式。
  5. 资源池需要上限。 数据库连接池和线程池可以复用资源,但过大的池、无界队列和不合适的超时会制造新的问题。
  6. 缓冲 I/O 先看已有层次。 适当缓冲可以减少细碎读写;重复包装和盲目加大缓冲区不一定有收益。
  7. 链表不一定更适合插入和删除。 如果先按索引找到位置,查找本身就有成本,还要考虑访问局部性和节点开销。
  8. 参数多时检查模型。 相关参数可以归入明确的请求对象,但没有“超过四个参数就错误”的通用规则。
  9. 字符串比较明确处理 null。 已知常量可写在前面,也可使用 Objects.equals(a, b) 表达可空值之间的比较。
  10. 条件表达式保持自然顺序。 Java 中整数赋值不能直接作为 if 的布尔条件;仍要注意布尔变量赋值这类合法但易误读的代码。
  11. 打印数组内容用对应工具。 一维数组用 Arrays.toString,嵌套数组可用 Arrays.deepToString,不要把数组对象标识当作内容。
  12. 窄化转换先检查范围。 long 强转 int 可能丢失高位;希望越界时报错时可用 Math.toIntExact。
  13. 共享集合中的数据要有退出机制。 缓存、订阅、会话不能只添加不移除,生命周期管理比换一种集合更关键。
  14. 字符串转换先看类型和空值语义。 int 没有实例 toString() 方法,Integer 则可能为 null。不要从一次毫秒计时得出通用速度排名。
  15. 遍历 Map 时拿自己需要的内容。 同时需要键和值时使用 entrySet();只需要键时用 keySet(),避免不必要的重复查询。
  16. 多资源关闭优先考虑 try-with-resources。 支持 AutoCloseable 的资源可以由它管理,异常信息也比多个空 catch 更容易保留。官方资源关闭教程

三、几个可以直接看懂的小例子

移位和除法并不总相等

java
System.out.println(-3 / 2);  // -1,整数除法向零截断
System.out.println(-3 >> 1); // -2,算术右移

同时读取 Map 的键和值

java
Map<String, Integer> counts = Map.of("success", 8, "failed", 2);
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

示例使用 Map.of,适用于 Java 9 及以上;实际文件需导入 java.util.Map。

读取文件并释放资源

java
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

这里的 path 是一个 Path 实例,外围方法需要处理 IOException,并导入对应的 I/O、NIO 类型。

四、性能结论怎么验证

先记录业务中的实际现象:响应延迟、吞吐、分配量、GC 或数据库查询。确认瓶颈后,再比较一个变量的变化,同时记录 JDK 版本、数据规模与运行参数。

微基准可以使用 OpenJDK JMH。测量时考虑预热、结果是否被使用以及多次采样;一次 currentTimeMillis() 前后相减,无法可靠比较几种极短操作。

原稿没有保留这份“35 条技巧”的作者与原文链接,因此这里标为重新整理,不把旧文中的固定收益数字当作实测结果。

文章配图