Skip to content

第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 对比

维度CJava
内存控制手动分配释放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 有哪些风险?
优化后应该保留哪些对比指标?