TECH NOTES / Java 与 Spring
Java 代码实践:35 条建议的重新整理
原笔记 2021-10-12 · 整理于 2026-10-05
原笔记叫“提高千倍效率的 Java 代码小技巧”。重新读一遍,有些建议是在讲代码习惯,有些是在讲资源管理,还有一些性能结论缺少成立条件。
这次保留原来的 35 个话题,重新整理说明和示例。这里不承诺某个写法必然快多少:先保证结果正确,再定位瓶颈,最后用数据判断优化有没有价值。
一、变量、对象与集合
final表达约束。 用它说明引用不再赋值、方法不可重写或类不可继承,不要据此推导“性能固定提高 50%”。- 对象复用要看场景。 在循环里逐步拼接大量字符串时可考虑
StringBuilder;不要为了省一个对象引入难以维护的共享状态。 - 缩小变量作用域。 局部变量有助于理解代码。不能仅凭变量写在方法内,就断言它引用的对象不需要垃圾回收。
- 及时关闭外部资源。 文件、连接与流需要按生命周期释放,不要把资源句柄管理交给 GC。
- 缓存计算结果要保证语义不变。 循环期间集合可能变化时,缓存
size()会改变遍历范围;ArrayList.size()本身是常数时间操作。 - 按需创建。 昂贵且不一定用到的对象可以延迟创建,但要考虑并发初始化和首次访问的延迟。
- 异常用于表达失败。 不要靠不断抛出异常完成正常分支判断;也不要为了少抛异常而吞掉真正的失败。
try/catch放哪里取决于业务。 一条失败要不要终止整个批次,决定了它应在循环内部还是外部,不能机械地挪动。- 容量预估需要合理。
ArrayList、HashMap和StringBuilder的容量不是同一个概念,LinkedList也没有初始容量参数。预估过大同样浪费内存。 - 数组复制优先使用现成 API。 根据是否需要新数组,考虑
System.arraycopy或Arrays.copyOf,先检查边界和类型。 - 不要机械地用移位替代除法。 例如负数的整数除法与算术右移可能得到不同结果,读得懂且语义正确更重要。
- 把引用移到循环外不等于少创建对象。 循环里仍然执行
new,创建次数就没有因此减少,还可能扩大变量作用域。 - 数组与集合按接口需求选择。 固定大小、底层数据处理可以使用数组;需要增删与集合操作时使用合适的集合。
- 线程安全不能只看类名。 单线程使用
HashMap、ArrayList很常见;并发环境要按访问方式设计同步或并发集合。 final数组仍可修改元素。 对外暴露可变数组时需要考虑防御性复制,而不是只给引用加final。- 单例也有生命周期和并发问题。 只在确实需要共享的场景使用,单例本身不保证其字段线程安全。
- 静态引用要有明确用途。 长期持有无用对象可能阻止回收;缓存应有容量、淘汰和清理策略。
- 会话按业务失效。 注销或权限变更时处理会话,超时配置结合实际系统,不能把某个容器默认值当成统一标准。
- 遍历方式结合数据结构。 索引遍历链表可能反复查找节点;普通业务先选清晰写法,热点路径再测量。ArrayList 与 LinkedList 的官方说明可以用来核对操作成本。
二、并发、接口与可读性
- 同步范围围绕共享不变量。 缩小锁范围的前提是不会破坏一致性,不能单纯为了“锁得少”拆开需要原子完成的操作。
- 常量有明确类型与语义。
static final很适合常量声明,但并非所有这类字段都是编译期常量。 - 清理无用代码。 删除不使用的局部变量、导入和死分支,主要收益是降低阅读和维护成本。
- 反射按需求使用。 框架接入与动态扩展常常需要反射;发现热点后再考虑缓存元数据或调整调用方式。
- 资源池需要上限。 数据库连接池和线程池可以复用资源,但过大的池、无界队列和不合适的超时会制造新的问题。
- 缓冲 I/O 先看已有层次。 适当缓冲可以减少细碎读写;重复包装和盲目加大缓冲区不一定有收益。
- 链表不一定更适合插入和删除。 如果先按索引找到位置,查找本身就有成本,还要考虑访问局部性和节点开销。
- 参数多时检查模型。 相关参数可以归入明确的请求对象,但没有“超过四个参数就错误”的通用规则。
- 字符串比较明确处理 null。 已知常量可写在前面,也可使用
Objects.equals(a, b)表达可空值之间的比较。 - 条件表达式保持自然顺序。 Java 中整数赋值不能直接作为
if的布尔条件;仍要注意布尔变量赋值这类合法但易误读的代码。 - 打印数组内容用对应工具。 一维数组用
Arrays.toString,嵌套数组可用Arrays.deepToString,不要把数组对象标识当作内容。 - 窄化转换先检查范围。
long强转int可能丢失高位;希望越界时报错时可用Math.toIntExact。 - 共享集合中的数据要有退出机制。 缓存、订阅、会话不能只添加不移除,生命周期管理比换一种集合更关键。
- 字符串转换先看类型和空值语义。
int没有实例toString()方法,Integer则可能为 null。不要从一次毫秒计时得出通用速度排名。 - 遍历 Map 时拿自己需要的内容。 同时需要键和值时使用
entrySet();只需要键时用keySet(),避免不必要的重复查询。 - 多资源关闭优先考虑 try-with-resources。 支持
AutoCloseable的资源可以由它管理,异常信息也比多个空catch更容易保留。官方资源关闭教程
三、几个可以直接看懂的小例子
移位和除法并不总相等
System.out.println(-3 / 2); // -1,整数除法向零截断
System.out.println(-3 >> 1); // -2,算术右移同时读取 Map 的键和值
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。
读取文件并释放资源
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 条技巧”的作者与原文链接,因此这里标为重新整理,不把旧文中的固定收益数字当作实测结果。