4. 代码级优化:算法复杂度分析、数据结构选择、循环优化、减少内存分配

代码级优化,说白了就是让每一行代码都发挥最大价值。很多人觉得优化是架构师的事,跟我写业务代码有什么关系?其实不然。我见过太多系统,架构设计得漂漂亮亮,结果一跑起来卡成PPT,问题全出在代码细节上。

这一章,我们就聊聊代码层面最核心的四个优化方向。嗯,都是我在实际项目中踩过坑、填过坑的经验。

4.1 算法复杂度分析:别让代码“慢”在起跑线上

先问个问题:你写的这段代码,数据量翻10倍,执行时间会翻多少倍?

如果你答不上来,那说明你还没养成复杂度分析的习惯。我个人习惯是,写任何一段核心逻辑之前,先在心里估算一下它的时间复杂度。

核心原则:能O(1)就别O(n),能O(n)就别O(n²)。数据量越大,复杂度差异越恐怖。

举个例子,我曾在项目中接手过一个报表模块。数据量只有几千条时跑得还行,但到了几万条,直接超时。一查代码,三层for循环嵌套,复杂度O(n³)。

常见的复杂度对比如下:

复杂度 数据量1000 数据量10000 典型场景
O(1) 1 1 哈希表查找
O(log n) 10 14 二分查找
O(n) 1000 10000 单层循环
O(n log n) 10000 140000 快速排序
O(n²) 1,000,000 100,000,000 双层循环

看到了吗?O(n²)在10000条数据时,操作次数已经上亿了。你想想看,这能不慢吗?

我的习惯:写代码前先画个草图,标出每个循环的数据规模。如果发现嵌套层数超过2层,立刻停下来想想有没有更好的方案。

4.2 数据结构选择:选对了,事半功倍

算法复杂度是“怎么做”,数据结构是“存什么”。两者密不可分。

我记得有一次做实时推荐系统,需要频繁地判断某个用户是否在在线列表中。同事用了ArrayList,每次判断都要遍历整个列表。数据量一上来,CPU直接飙到100%。

我建议他换成HashSet,判断操作从O(n)降到了O(1)。问题瞬间解决。

不同数据结构的适用场景差异很大:

数据结构 查找 插入 删除 适用场景
数组 O(1) O(n) O(n) 固定大小、频繁随机访问
链表 O(n) O(1) O(1) 频繁插入删除
哈希表 O(1) O(1) O(1) 快速查找、去重
二叉搜索树 O(log n) O(log n) O(log n) 有序数据、范围查询
堆 O(1) O(log n) O(log n) 优先级队列、TopK

避坑指南:我曾经在项目中用HashMap存储大量小对象,结果内存占用爆炸。原因是HashMap的负载因子和扩容机制导致内存浪费严重。后来改用数组+自定义哈希,内存降了40%。

4.3 循环优化:把每一轮都榨干

循环是代码里最常见的结构,也是最容易藏性能问题的地方。我总结了几条实用的优化技巧:

  • 减少循环内的函数调用:把不变的表达式提到循环外面。比如 for (int i = 0; i < list.size(); i++) 改成 int size = list.size(); for (int i = 0; i < size; i++)。
  • 避免重复计算:循环内如果有关联数据库或远程调用的操作,尽量批量处理。
  • 使用局部变量:频繁访问的成员变量,先赋值给局部变量再操作。局部变量在栈上,访问速度比堆上的成员变量快得多。
  • 循环展开:对于固定次数的循环,手动展开可以减少循环控制开销。
// 优化前
for (int i = 0; i < 1000; i++) {
    process(data[i]);
}

// 优化后:循环展开
for (int i = 0; i < 1000; i += 4) {
    process(data[i]);
    process(data[i+1]);
    process(data[i+2]);
    process(data[i+3]);
}

小技巧:如果你用Java,试试用 for-each 替代传统for循环。大部分情况下编译器会帮你优化成更高效的迭代器模式。但如果是ArrayList,传统for循环反而更快,因为可以直接用索引访问。

4.4 减少内存分配:GC的敌人就是你的朋友

内存分配看似小事,但频繁分配会带来两个问题:一是分配本身有开销,二是触发GC(垃圾回收)导致停顿。

我经历过一个极端案例:一个高并发服务,每秒处理上万请求。代码里每个请求都new了一个临时对象。结果GC频率高到离谱,服务经常出现“世界暂停”。

优化思路其实很简单:

  • 对象复用:用对象池或线程局部变量,避免频繁创建和销毁。
  • 使用基本类型:能用int就别用Integer,能避免装箱拆箱就避免。
  • StringBuilder代替字符串拼接:循环里拼接字符串,一定要用StringBuilder。
  • 延迟初始化:对象真正需要时才创建,别提前new好。
// 优化前:每次循环都创建新对象
for (int i = 0; i < 10000; i++) {
    StringBuilder sb = new StringBuilder();
    sb.append("prefix").append(i);
    process(sb.toString());
}

// 优化后:复用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("prefix").append(i);
    process(sb.toString());
}

注意:对象池不是万能的。如果对象创建成本很低,池化反而增加复杂度。我曾经见过有人把Integer也池化,结果性能没提升,代码却变得难以维护。适度复用,别过度设计。

4.5 知识体系总览

下面这张图,把代码级优化的四个维度串起来了。你可以把它当作一个检查清单,每次写完代码后对照着过一遍。

代码级优化知识体系 代码级优化 算法复杂度 数据结构选择 循环优化 减少内存分配 O(1) / O(n) / O(n²) 数组 / 哈希 / 树 减少调用 / 展开 对象复用 / 池化 优化检查清单 1. 核心逻辑是否做了复杂度分析? 2. 数据结构是否匹配访问模式? 3. 循环内是否有不必要的操作? 4. 是否频繁创建临时对象? 5. 能否用基本类型替代包装类? 6. 字符串拼接是否用了StringBuilder?

代码级优化,说到底就是四个字:少做、快做。少做无用功,快做必要功。每次写完代码,对照上面的清单过一遍,你会发现性能提升其实没那么难。