"同样的功能,为什么你的代码跑起来像蜗牛,大佬的代码却快如闪电?" 🐌 vs ⚡
📖 什么是代码层面的优化?
想象你在做饭:
- 菜鸟:每次炒一个菜,炒完洗锅,再炒下一个(慢!)🍳
- 高手:食材提前备好,多个菜同时进行,一气呵成(快!)👨🍳
代码优化就是这样:同样的功能,用更高效的方式实现!
🎯 优化原则
优化三原则:
1️⃣ 先保证正确性,再考虑性能
(错误的快代码 < 正确的慢代码)
2️⃣ 测量优先(Measure First)
(不要凭感觉,用数据说话)
3️⃣ 优化二八原则
(20%的代码占用80%的执行时间)
🔥 优化技巧一:字符串拼接
❌ 反面教材
// 循环中使用 + 拼接字符串(性能杀手!)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ","; // 每次都创建新的String对象!
}
// 时间:约 3000ms
为什么慢?
第1次:result = "" + "0," → 创建对象1
第2次:result = "0," + "1," → 创建对象2
第3次:result = "0,1," + "2," → 创建对象3
...
第10000次: → 创建对象10000
总共创建了 10000 个 String 对象!
每次都要:
1. 创建新对象
2. 复制旧内容
3. 添加新内容
4. 旧对象等待GC回收
就像搬家:每次加一件东西就搬一次家!😱
✅ 正确姿势
// 方式1:StringBuilder(单线程)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();
// 时间:约 3ms
// 性能提升:1000倍!⚡
// 方式2:StringBuffer(多线程安全,稍慢)
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();
// 时间:约 5ms
// 方式3:String.join(JDK 8+,最优雅)
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(String.valueOf(i));
}
String result = String.join(",", list);
// 时间:约 5ms
🎓 进阶技巧
// ✅ 预分配容量(避免扩容)
StringBuilder sb = new StringBuilder(10000 * 6); // 预估容量
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
// StringBuilder 默认容量 16,扩容策略:capacity * 2 + 2
// 如果不预分配:
// 16 → 34 → 70 → 142 → 286 → 574 → 1150 → ...
// 每次扩容都要复制数据!
对比表格:
| 方式 | 10000次拼接耗时 | 内存占用 | 推荐场景 |
|---|---|---|---|
+ 拼接 | 3000ms | 高 | ❌ 禁用 |
StringBuilder | 3ms | 低 | ✅ 单线程 |
StringBuffer | 5ms | 低 | 多线程 |
String.join | 5ms | 中 | 集合拼接 |
🔥 优化技巧二:集合操作
ArrayList vs LinkedList
// 场景1:随机访问
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 添加10万个元素
for (int i = 0; i < 100000; i++) {
arrayList.add(i);
linkedList.add(i);
}
// ❌ LinkedList 随机访问慢(O(n))
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int value = linkedList.get(i); // 每次都要从头遍历!
}
System.out.println("LinkedList: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 500ms
// ✅ ArrayList 随机访问快(O(1))
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int value = arrayList.get(i); // 数组下标直接访问
}
System.out.println("ArrayList: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 1ms
选择原则:
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 随机访问(get) | O(1) ⭐ | O(n) ❌ |
| 头部插入/删除 | O(n) | O(1) ⭐ |
| 尾部插入/删除 | O(1) ⭐ | O(1) ⭐ |
| 中间插入/删除 | O(n) | O(n) |
| 内存占用 | 低 | 高(节点指针) |
结论:99%的场景用 ArrayList!
初始化容量
// ❌ 不指定容量(频繁扩容)
List<String> list = new ArrayList<>(); // 默认容量10
for (int i = 0; i < 10000; i++) {
list.add("item" + i);
// 扩容发生在:10 → 15 → 22 → 33 → 49 → 73 → ...
// 每次扩容都要复制所有元素!
}
// ✅ 预估容量(一次分配到位)
List<String> list = new ArrayList<>(10000);
for (int i = 0; i < 10000; i++) {
list.add("item" + i); // 不需要扩容
}
// 性能对比:
// 不指定容量:约 15ms
// 指定容量:约 5ms(快3倍)
集合去重
// ❌ 使用 contains 去重(O(n²),太慢!)
List<String> list = Arrays.asList("a", "b", "c", "b", "a", "d");
List<String> result = new ArrayList<>();
for (String item : list) {
if (!result.contains(item)) { // O(n) 的查找
result.add(item);
}
}
// 时间复杂度:O(n²)
// ✅ 使用 Set 去重(O(n))
List<String> list = Arrays.asList("a", "b", "c", "b", "a", "d");
Set<String> set = new HashSet<>(list); // O(n)
List<String> result = new ArrayList<>(set);
// 时间复杂度:O(n)
// ✅ Stream 去重(优雅且高效)
List<String> result = list.stream()
.distinct()
.collect(Collectors.toList());
🔥 优化技巧三:对象创建与复用
避免不必要的对象创建
// ❌ 循环中创建对象(每次都new)
for (int i = 0; i < 10000; i++) {
String s = new String("hello"); // 创建10000个对象
System.out.println(s);
}
// ✅ 对象复用(只创建一次)
String s = "hello"; // 字符串常量池,共享
for (int i = 0; i < 10000; i++) {
System.out.println(s);
}
// ❌ 包装类型自动装箱(隐式创建对象)
Integer sum = 0;
for (int i = 0; i < 10000; i++) {
sum += i; // 每次都拆箱、计算、装箱!
}
// ✅ 使用基本类型
int sum = 0;
for (int i = 0; i < 10000; i++) {
sum += i; // 纯粹的加法运算
}
对象池模式
// 场景:频繁创建和销毁对象
// ❌ 每次都new
for (int i = 0; i < 10000; i++) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String date = sdf.format(new Date());
// SimpleDateFormat 创建成本高!
}
// ✅ 对象池复用(Apache Commons Pool)
GenericObjectPool<SimpleDateFormat> pool = new GenericObjectPool<>(
new BasePooledObjectFactory<SimpleDateFormat>() {
@Override
public SimpleDateFormat create() {
return new SimpleDateFormat("yyyy-MM-dd");
}
@Override
public PooledObject<SimpleDateFormat> wrap(SimpleDateFormat obj) {
return new DefaultPooledObject<>(obj);
}
}
);
for (int i = 0; i < 10000; i++) {
SimpleDateFormat sdf = pool.borrowObject(); // 从池中借
try {
String date = sdf.format(new Date());
} finally {
pool.returnObject(sdf); // 归还到池
}
}
// 性能提升:5-10倍
ThreadLocal 复用(线程私有)
// ✅ 更优雅的方案:ThreadLocal
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return FORMATTER.get().format(date); // 线程内复用
}
}
// 使用
for (int i = 0; i < 10000; i++) {
String date = DateUtils.format(new Date());
}
// ⚠️ 注意:用完要 remove(),防止内存泄漏!
🔥 优化技巧四:Stream API 性能
Stream vs 传统循环
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
// ❌ Stream(小数据量下更慢)
long start = System.currentTimeMillis();
long sum = list.stream()
.filter(i -> i % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println("Stream: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 80ms
// ✅ 传统 for 循环(更快)
start = System.currentTimeMillis();
long sum2 = 0;
for (int i : list) {
if (i % 2 == 0) {
sum2 += i;
}
}
System.out.println("For: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 10ms
// 为什么 Stream 慢?
// 1. 对象包装/拆箱
// 2. 函数调用开销
// 3. 惰性求值的额外逻辑
并行 Stream(大数据量下有优势)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) { // 1000万数据
list.add(i);
}
// 串行 Stream
long start = System.currentTimeMillis();
long sum = list.stream()
.filter(i -> i % 2 == 0)
.mapToLong(Integer::longValue)
.sum();
System.out.println("串行: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 800ms
// ✅ 并行 Stream(多核CPU加速)
start = System.currentTimeMillis();
sum = list.parallelStream()
.filter(i -> i % 2 == 0)
.mapToLong(Integer::longValue)
.sum();
System.out.println("并行: " + (System.currentTimeMillis() - start) + "ms");
// 耗时:约 200ms(4核CPU,提升4倍)
使用建议:
- 数据量 < 1万:用 for 循环
- 数据量 > 10万 且 计算密集:用 parallelStream
- 其他场景:看情况,优先考虑可读性
🔥 优化技巧五:反射优化
反射的性能问题
public class User {
private String name;
public void setName(String name) { this.name = name; }
}
// ❌ 每次都反射查找方法(超慢!)
for (int i = 0; i < 100000; i++) {
User user = new User();
Method method = user.getClass().getMethod("setName", String.class);
method.invoke(user, "张三");
}
// 耗时:约 500ms
// ✅ 缓存 Method 对象
Method cachedMethod = User.class.getMethod("setName", String.class);
cachedMethod.setAccessible(true); // 跳过安全检查
for (int i = 0; i < 100000; i++) {
User user = new User();
cachedMethod.invoke(user, "张三");
}
// 耗时:约 50ms(快10倍)
// ✅✅ 直接调用(最快)
for (int i = 0; i < 100000; i++) {
User user = new User();
user.setName("张三");
}
// 耗时:约 5ms(快100倍)
MethodHandle(JDK 7+,更快的反射)
public class User {
private String name;
public void setName(String name) { this.name = name; }
}
// ✅ 使用 MethodHandle(比反射快3-5倍)
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle mh = lookup.findVirtual(User.class, "setName", mt);
for (int i = 0; i < 100000; i++) {
User user = new User();
mh.invoke(user, "张三");
}
// 耗时:约 15ms
性能对比:
| 方式 | 耗时 | 相对性能 |
|---|---|---|
| 直接调用 | 5ms | 100% ⭐⭐⭐ |
| MethodHandle | 15ms | 33% ⭐⭐ |
| 反射(缓存Method) | 50ms | 10% ⭐ |
| 反射(不缓存) | 500ms | 1% ❌ |
🔥 优化技巧六:异常处理
异常的性能开销
// ❌ 用异常控制流程(性能杀手!)
for (int i = 0; i < 10000; i++) {
try {
int result = 10 / i; // i=0 时抛异常
} catch (ArithmeticException e) {
// 捕获异常
}
}
// 耗时:约 100ms
// ✅ 用 if 判断(快100倍)
for (int i = 0; i < 10000; i++) {
if (i != 0) {
int result = 10 / i;
}
}
// 耗时:约 1ms
为什么异常慢?
抛出异常时,JVM 要做:
1. 创建异常对象
2. 收集调用栈信息(fillInStackTrace)
3. 查找 catch 块
4. 展开调用栈
就像警察破案:
if 判断 = 在门口检查证件(快)
异常 = 放进来后发现有问题,回溯调查(慢)
优化异常创建
// ❌ 频繁创建异常(每次都收集栈)
for (int i = 0; i < 10000; i++) {
throw new RuntimeException("error");
}
// ✅ 复用异常对象(不收集栈)
public class FastException extends RuntimeException {
@Override
public synchronized Throwable fillInStackTrace() {
return this; // 不填充栈信息
}
}
FastException cached = new FastException("error");
for (int i = 0; i < 10000; i++) {
throw cached; // 复用同一个对象
}
// 性能提升:10倍
🔥 优化技巧七:正则表达式
Pattern 预编译
// ❌ 每次都编译正则(慢)
for (int i = 0; i < 10000; i++) {
boolean match = "123-456-7890".matches("\d{3}-\d{3}-\d{4}");
}
// 耗时:约 500ms
// ✅ 预编译 Pattern(快100倍)
Pattern pattern = Pattern.compile("\d{3}-\d{3}-\d{4}");
for (int i = 0; i < 10000; i++) {
Matcher matcher = pattern.matcher("123-456-7890");
boolean match = matcher.matches();
}
// 耗时:约 5ms
// ✅✅ 复用 Matcher(最快)
Pattern pattern = Pattern.compile("\d{3}-\d{3}-\d{4}");
Matcher matcher = pattern.matcher("");
for (int i = 0; i < 10000; i++) {
matcher.reset("123-456-7890");
boolean match = matcher.matches();
}
// 耗时:约 3ms
🔥 优化技巧八:日期时间
SimpleDateFormat 线程不安全
// ❌ 共享 SimpleDateFormat(线程不安全!)
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用会出错
public String format(Date date) {
return SDF.format(date); // 并发时会乱套!
}
// ✅ 方案1:每次 new(慢,但安全)
public String format(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
// ✅ 方案2:ThreadLocal(推荐)
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String format(Date date) {
return SDF.get().format(date);
}
// ✅✅ 方案3:DateTimeFormatter(JDK 8+,线程安全)
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String format(LocalDate date) {
return date.format(FORMATTER); // 天生线程安全!
}
📊 性能优化对照表
| 优化点 | ❌ 不好的做法 | ✅ 好的做法 | 性能提升 |
|---|---|---|---|
| 字符串拼接 | + 拼接 | StringBuilder | 100-1000倍 |
| 集合初始化 | 不指定容量 | new ArrayList<>(size) | 2-3倍 |
| 集合选择 | LinkedList | ArrayList | 10-100倍 |
| 对象创建 | 循环中 new | 对象复用/池化 | 5-10倍 |
| 反射调用 | 不缓存 Method | 缓存或 MethodHandle | 10-100倍 |
| 异常处理 | 用异常控制流程 | if 判断 | 100倍 |
| 正则表达式 | String.matches() | 预编译 Pattern | 100倍 |
| 日期格式化 | 每次 new | ThreadLocal 或 DateTimeFormatter | 5-10倍 |
💡 面试加分回答模板
面试官:"你在代码层面做过哪些性能优化?"
标准回答:
"我主要从以下几个方面进行代码优化:
1. 字符串拼接:
- 循环中避免使用
+拼接,改用StringBuilder- 实际案例:日志拼接从 3秒 优化到 3毫秒
2. 集合操作:
- 初始化时指定容量,避免频繁扩容
- 99%的场景用
ArrayList而不是LinkedList- 去重用
HashSet而不是contains()3. 对象复用:
- 频繁创建的对象用 ThreadLocal 或对象池
- 例如 SimpleDateFormat、数据库连接
4. 反射优化:
- 缓存 Method 对象,避免重复查找
- 能用直接调用就不用反射
5. 异常处理:
- 不用异常控制业务流程
- 能用 if 判断就不抛异常
实际案例:我曾经优化过一个报表生成接口,发现瓶颈在字符串拼接和反射调用。通过改用 StringBuilder 和缓存 Method,性能从 30秒 优化到 3秒,提升了 10倍。"
🎯 优化流程总结
代码优化四步法:
┌──────────────────────┐
│ 1. 测量(Measure) │
│ - 性能测试 │
│ - Profiler 分析 │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 2. 定位(Locate) │
│ - 找到热点代码 │
│ - 20% 的代码 │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 3. 优化(Optimize) │
│ - 应用技巧 │
│ - 重构代码 │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ 4. 验证(Verify) │
│ - 再次测量 │
│ - 对比效果 │
└──────────────────────┘
🎉 总结金句
- 过早优化是万恶之源 - 先保证正确,再追求性能 ✅
- 测量优先,数据说话 - 不要凭感觉优化 📊
- 优化热点代码 - 抓主要矛盾 🎯
- 可读性 > 性能 - 除非性能真的是瓶颈 📖
- 工具辅助分析 - Profiler、JMH 是你的好朋友 🔧
最后一句话:
优化的最高境界:
不是写出最快的代码
而是写出又快又易读的代码!
Keep It Simple, Stupid (KISS) 🎯
祝你的代码又快又优雅! ⚡✨
📚 扩展阅读