Appearance
第70课:代码优化
🎯 学习目标
- 理解代码优化的前提是算法、数据结构和调用链,而不是盲目微调语法。
- 掌握集合选择、对象创建、字符串处理、异常使用、IO 和批处理的常见优化方向。
- 能识别会导致 CPU、内存、GC 或响应时间恶化的代码模式。
- 能用基准测试和压测验证优化效果。
- 建立“可读性优先,热点路径再优化”的工程判断。
📖 一、代码优化的层级
代码优化有优先级:
text
1. 算法复杂度
2. 数据结构选择
3. 数据访问次数
4. 批量化和缓存
5. 对象创建和内存分配
6. 语法级微优化把 for 改成 Stream 通常不是关键;把 O(n²) 改成 O(n) 才是关键。
示例:
java
// O(n²)
for (User user : users) {
for (Order order : orders) {
if (order.getUserId().equals(user.getId())) {
user.addOrder(order);
}
}
}优化为:
java
// O(n)
Map<Long, List<Order>> ordersByUserId = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
for (User user : users) {
user.setOrders(ordersByUserId.getOrDefault(user.getId(), List.of()));
}这种优化通常比任何语法微调都有效。
📖 二、选择合适的数据结构
1. List 查找问题
错误示例:
java
boolean exists = false;
for (Long id : idList) {
if (id.equals(targetId)) {
exists = true;
break;
}
}如果频繁判断存在性,应使用 Set:
java
Set<Long> idSet = new HashSet<>(idList);
boolean exists = idSet.contains(targetId);2. Map 替代重复查询
java
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
User user = userMap.get(order.getUserId());3. 预估容量
大集合可以预估容量,减少扩容:
java
List<UserDTO> result = new ArrayList<>(users.size());
Map<Long, User> map = new HashMap<>((int) (users.size() / 0.75f) + 1);但不要为了小集合到处写复杂容量公式,可读性不值得。
📖 三、减少无意义对象创建
1. 循环中创建重复对象
错误示例:
java
for (Order order : orders) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
order.setTimeText(formatter.format(order.getCreatedAt()));
}优化:
java
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (Order order : orders) {
order.setTimeText(FORMATTER.format(order.getCreatedAt()));
}2. 不必要的包装对象
java
Long count = 0L;
for (Order order : orders) {
count++; // 频繁装箱拆箱
}热点路径可使用基本类型:
java
long count = 0;3. 过度使用 Optional
Optional 适合作为返回值表达“可能为空”,不适合字段、参数和高频循环里的临时对象。
📖 四、字符串处理
1. 循环拼接
错误示例:
java
String result = "";
for (String item : items) {
result += item + ",";
}优化:
java
StringBuilder builder = new StringBuilder();
for (String item : items) {
builder.append(item).append(",");
}
String result = builder.toString();更推荐:
java
String result = String.join(",", items);2. 日志字符串拼接
错误示例:
java
log.debug("user list: " + users);优化:
java
log.debug("user list: {}", users);DEBUG 关闭时,参数化日志能避免提前拼接字符串。
📖 五、异常使用
异常适合表达异常路径,不适合控制正常流程。
错误示例:
java
try {
int value = Integer.parseInt(text);
return value;
} catch (NumberFormatException e) {
return 0;
}如果这是高频正常分支,应该先判断:
java
if (!text.matches("\\d+")) {
return 0;
}
return Integer.parseInt(text);但注意:正则本身也可能昂贵,热点路径可以写更直接的字符判断。
java
private boolean isDigits(String text) {
if (text == null || text.isEmpty()) {
return false;
}
for (int i = 0; i < text.length(); i++) {
if (!Character.isDigit(text.charAt(i))) {
return false;
}
}
return true;
}📖 六、批量处理
1. 批量数据库操作
错误示例:
java
for (User user : users) {
userRepository.save(user);
}优化:
java
userRepository.saveAll(users);更底层时可以使用 JDBC batch。
2. 批量远程调用
错误示例:
java
for (Long userId : userIds) {
UserProfile profile = userClient.getProfile(userId);
}优化方向:
text
提供批量接口
本地缓存热点数据
并发调用但限制并发数
异步化并设置超时和降级远程调用的优化收益通常远大于本地代码微调。
📖 七、Stream 的性能判断
Stream 提升表达力,但不是所有场景都更快。
适合:
java
List<String> names = users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();不适合:
java
for (int i = 0; i < 10_000_000; i++) {
// 极端热点数值计算
}对于普通业务代码,可读性优先。只有 profiler 证明 Stream 是热点,再考虑改写。
📖 八、JMH 基准测试
微基准不要直接写 System.currentTimeMillis() 循环测试。JIT、逃逸分析、死代码消除会让结果失真。
JMH 示例:
java
@Benchmark
public String stringBuilderJoin() {
StringBuilder builder = new StringBuilder();
for (String item : items) {
builder.append(item).append(",");
}
return builder.toString();
}JMH 会处理预热、多轮采样、JIT 等问题。真实接口性能仍要通过压测验证。
⚠️ 九、常见陷阱
1. 牺牲可读性的过早优化
如果一段代码不在热点路径,复杂优化只会增加维护成本。
2. 只优化 Java 代码,不看外部依赖
接口 900ms 中 850ms 花在 SQL 和 HTTP 调用,本地循环优化没有意义。
3. 缓存无边界
java
static Map<String, Object> cache = new HashMap<>();没有容量、TTL、清理策略的缓存最终会变成内存泄漏。
4. 并行流误用
parallelStream() 使用公共 ForkJoinPool,可能影响同 JVM 内其他任务。IO 密集型任务也不一定适合直接并行流。
🆚 十、Java vs C 对比
| 维度 | C | Java |
|---|---|---|
| 内存控制 | 手动分配释放 | JVM 自动管理,关注对象分配和 GC |
| 编译优化 | 编译器静态优化 | JIT 运行时优化 |
| 字符串 | char 数组/指针 | 不可变 String + StringBuilder |
| 容器 | 手写或库 | JDK 集合丰富,但要选对结构 |
Java 的优化重点常常是减少不必要对象、降低 GC 压力、减少远程/数据库调用,而不是手动管理内存。
💡 十一、最佳实践
- 先用 profiler 找热点,再优化代码。
- 优先优化算法复杂度和调用次数。
- 高频存在性判断用 Set,按 ID 查找用 Map。
- 批量处理数据库和远程调用,避免循环调用外部资源。
- 循环拼接字符串使用 StringBuilder 或 String.join。
- 异常只用于异常流程,不用于高频正常分支。
- 缓存必须有容量、过期和一致性策略。
- 优化后保留对比数据,避免“看起来更快”。
🎓 小结
代码优化的本质是减少无效工作:少查、少算、少分配、少等待。真正有效的优化通常来自算法、数据结构、批量化、缓存和外部调用治理,而不是语法层面的细枝末节。
🧭 十二、代码优化检查清单
优化一段业务代码前,可以逐项检查:
text
1. 是否存在 O(n²) 或更高复杂度的循环?
2. 是否在循环里访问数据库、Redis 或 HTTP?
3. 是否可以把多次查询合并成批量查询?
4. 是否使用 List 做频繁 contains?
5. 是否在热点路径频繁创建格式化器、正则、临时集合?
6. 是否在 DEBUG 日志中使用字符串拼接?
7. 是否用异常表达正常分支?
8. 是否返回了过多字段或过大对象?
9. 是否有无界缓存或静态集合?
10. 优化点是否经过 profiler 或压测证明?一个常见接口优化顺序:
text
先消除 N+1 查询。
再减少返回字段和数据量。
再批量化远程调用。
再选择更合适的数据结构。
最后才考虑语法级微优化。这能避免把时间花在收益最小的地方。
📌 十三、可读性与性能的平衡
大多数业务代码应该优先清晰:
java
List<UserDTO> result = users.stream()
.filter(User::isActive)
.map(UserDTO::from)
.toList();如果 profiler 证明这里是热点,再改写为更底层的循环:
java
List<UserDTO> result = new ArrayList<>(users.size());
for (User user : users) {
if (user.isActive()) {
result.add(UserDTO.from(user));
}
}不要因为“for 可能更快”就让所有代码都变得冗长。工程优化必须服务于真实瓶颈。
🔍 十四、自测问题
text
为什么算法复杂度通常比语法微优化更重要?
什么时候 List 应该换成 Set?
为什么循环里调用远程接口是危险信号?
为什么异常不适合控制高频正常流程?
为什么 StringBuilder 不一定要在所有字符串拼接场景手写?
JMH 解决了普通计时循环的哪些问题?
parallelStream 有哪些风险?
优化后应该保留哪些对比指标?