💻 代码优化宝典:从菜鸟到高手的进阶秘籍!

68 阅读10分钟

"同样的功能,为什么你的代码跑起来像蜗牛,大佬的代码却快如闪电?" 🐌 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❌ 禁用
StringBuilder3ms✅ 单线程
StringBuffer5ms多线程
String.join5ms集合拼接

🔥 优化技巧二:集合操作

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

选择原则

操作ArrayListLinkedList
随机访问(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

性能对比

方式耗时相对性能
直接调用5ms100% ⭐⭐⭐
MethodHandle15ms33% ⭐⭐
反射(缓存Method)50ms10% ⭐
反射(不缓存)500ms1% ❌

🔥 优化技巧六:异常处理

异常的性能开销

// ❌ 用异常控制流程(性能杀手!)
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);  // 天生线程安全!
}

📊 性能优化对照表

优化点❌ 不好的做法✅ 好的做法性能提升
字符串拼接+ 拼接StringBuilder100-1000倍
集合初始化不指定容量new ArrayList<>(size)2-3倍
集合选择LinkedListArrayList10-100倍
对象创建循环中 new对象复用/池化5-10倍
反射调用不缓存 Method缓存或 MethodHandle10-100倍
异常处理用异常控制流程if 判断100倍
正则表达式String.matches()预编译 Pattern100倍
日期格式化每次 newThreadLocal 或 DateTimeFormatter5-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)    │
│     - 再次测量        │
│     - 对比效果        │
└──────────────────────┘

🎉 总结金句

  1. 过早优化是万恶之源 - 先保证正确,再追求性能 ✅
  2. 测量优先,数据说话 - 不要凭感觉优化 📊
  3. 优化热点代码 - 抓主要矛盾 🎯
  4. 可读性 > 性能 - 除非性能真的是瓶颈 📖
  5. 工具辅助分析 - Profiler、JMH 是你的好朋友 🔧

最后一句话

优化的最高境界:
  不是写出最快的代码
  而是写出又快又易读的代码!

Keep It Simple, Stupid (KISS) 🎯

祝你的代码又快又优雅! ⚡✨


📚 扩展阅读