订阅更新

String拼接在循环里为什么慢?——String底层原理

Java手册 第 13 题 约 8 分钟读完

本文目录
  1. 先说结论
  2. String为什么设计成不可变的?
  3. String拼接的性能陷阱
  4. String的底层存储——JDK 8 vs JDK 9+
  5. 字符串常量池(String Pool)
  6. 实战经验:String拼接导致UDF变慢
  7. 面试高频追问
  8. 总结

先说结论

String在Java中是不可变的(immutable)。每次修改(拼接、替换等)都会创建一个新的String对象,旧对象成为垃圾等待GC。在循环中用'+'拼接String会产生大量临时对象——用StringBuilder替代可以避免这个问题。String的不可变性保证了线程安全和字符串常量池的复用,但也带来了频繁创建对象的开销。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

String看起来是最简单的Java类型,但面试官总能在这里挖出深度。大数据场景中String使用极其频繁——SQL文本、JSON解析、日志拼接、Key构造……在某知名出海硬件品牌做Spark ETL调优时,发现一个UDF里在循环中用'+'拼接String构造SQL,10万次循环产生了10万个临时String对象,GC压力巨大。改成StringBuilder后内存使用降了60%,UDF执行快了3倍。

String为什么设计成不可变的?

原因说明大数据场景
线程安全多线程共享String不需要加锁Spark/Flink多线程环境中共享字符串安全
HashCode缓存hash值只需要计算一次就可以缓存HashMap/HashSet中String作为Key极其高效
字符串常量池相同内容的String可以共享同一个对象大量重复字符串(event_name等)节省内存
安全性String常用于类名/文件路径/SQL,不可变防止篡改SQL注入防护、类加载安全

String拼接的性能陷阱

// 错误写法:循环中用'+'拼接(每次创建新String对象)
String sql = '';
for (int i = 0; i < 100000; i++) {
    sql = sql + 'UNION ALL SELECT ' + i;
    // 每次'+'都创建一个新String对象
    // 旧String成为垃圾,等待GC
    // 10万次循环 = 10万个临时对象!
}
// 时间复杂度:O(N^2)——每次拼接都要拷贝前面的全部内容
 
// 正确写法:用StringBuilder(可变字符串)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
    sb.append('UNION ALL SELECT ').append(i);
    // StringBuilder内部是可变的char数组
    // append只是在数组后面追加,不创建新对象
}
String sql = sb.toString();
// 时间复杂度:O(N)——只在最后toString时创建一个String
 
// 性能差距:10万次拼接
// String '+': 约15秒(大量GC)
// StringBuilder: 约5毫秒(几乎没有GC)
// 差距3000倍!

String的底层存储——JDK 8 vs JDK 9+

// JDK 8: String内部用char[]存储(每个字符2字节UTF-16)
// 'hello'占10字节的char数组
 
// JDK 9+: String内部用byte[]存储(Compact Strings)
// 如果全是Latin-1字符(ASCII),用1字节/字符
// 如果包含非Latin-1字符(中文等),用2字节/字符
// 'hello'只占5字节的byte数组(省50%内存!)
 
// 大数据的影响:
// 大量英文字段名(user_id/event_name/country等)
// JDK 9+的Compact Strings可以节省大量内存
// 如果你的Flink/Spark还在JDK 8上,升级到JDK 11/17
// 光String内存就能省20-30%

字符串常量池(String Pool)

// 字符串常量池:相同内容的String共享同一个对象
String s1 = 'hello';       // 放入常量池
String s2 = 'hello';       // 从常量池获取,和s1是同一个对象
System.out.println(s1 == s2);  // true(同一个对象)
 
String s3 = new String('hello'); // 在堆上创建新对象
System.out.println(s1 == s3);    // false(不同对象)
System.out.println(s1.equals(s3)); // true(内容相同)
 
// intern()方法:手动放入常量池
String s4 = s3.intern();
System.out.println(s1 == s4);  // true(从常量池获取)
 
// 大数据场景的应用:
// 事件名('page_view','click','purchase'等)大量重复
// 用intern()可以让所有相同的事件名共享一个String对象
// 减少内存占用
// 但注意:intern()有性能开销,不要在高频路径上调用

实战经验:String拼接导致UDF变慢

在某知名出海硬件品牌,有一个Spark UDF根据用户的标签列表生成SQL的IN条件。原来的写法:

// 原始UDF代码(String '+'拼接)
public String buildInClause(List<String> tags) {
    String result = 'IN (';
    for (int i = 0; i < tags.size(); i++) {
        result = result + "'" + tags.get(i) + "'";
        if (i < tags.size() - 1) result = result + ',';
    }
    return result + ')';
}
// 每个用户平均100个标签
// 500万用户 = 5亿次String拼接 = 5亿个临时对象
// Executor频繁Minor GC,UDF执行非常慢
 
// 优化后(StringBuilder)
public String buildInClause(List<String> tags) {
    StringBuilder sb = new StringBuilder('IN (');
    for (int i = 0; i < tags.size(); i++) {
        if (i > 0) sb.append(',');
        sb.append("'").append(tags.get(i)).append("'");
    }
    return sb.append(')').toString();
}
// 每个用户只创建1个StringBuilder对象
// GC压力减少99%,UDF执行快3倍

面试高频追问

追问一:String s = 'a' + 'b' + 'c' 会创建几个对象?

编译器会优化为String s = 'abc',只有1个对象(直接在常量池中)。但如果是String s = a + b + c(变量拼接),编译器会用StringBuilder优化:new StringBuilder().append(a).append(b).append(c).toString(),创建1个StringBuilder + 1个最终String = 2个对象。在循环中每次迭代都会new一个StringBuilder,所以循环拼接还是要手动用一个StringBuilder。

追问二:StringBuilder和StringBuffer有什么区别?

StringBuffer是线程安全的(方法加了synchronized),StringBuilder不是。单线程场景用StringBuilder更快(不需要锁的开销)。大数据场景中UDF通常是单线程执行的,用StringBuilder就够了。

追问三:为什么String的hashCode()方法用31作为乘数?

31是一个奇素数:① 素数减少hash冲突;② 31*i可以被JVM优化为(i << 5) - i(位运算比乘法快);③ 31不大不小——太小hash分布不均,太大容易溢出。这个选择是性能和分布均匀性的平衡。HashMap能高效运作离不开String的好hash。

总结

String面试讲清楚三层:① 不可变性的原因和好处(线程安全/hashCode缓存/常量池复用/安全);② 拼接性能陷阱(循环中用'+'是O(N^2),用StringBuilder是O(N),差距可达3000倍);③ 底层存储(JDK 9+ Compact Strings省50%内存,字符串常量池共享相同内容)。能讲出UDF中String拼接导致GC压力大的实际案例和StringBuilder优化效果,面试官会觉得你注意过代码级别的性能细节。

下一篇聊Java集合框架:ArrayList和LinkedList到底选哪个?大数据场景的集合选型。

你们代码里有没有String拼接的性能坑?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器