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 知识体系总览
下面这张图,把代码级优化的四个维度串起来了。你可以把它当作一个检查清单,每次写完代码后对照着过一遍。
代码级优化,说到底就是四个字:少做、快做。少做无用功,快做必要功。每次写完代码,对照上面的清单过一遍,你会发现性能提升其实没那么难。