4、字符串与常量池:String常量池、字符串拼接优化、Kotlin中字符串模板的内存影响
字符串,可以说是我们写代码时打交道最多的数据类型了。但说实话,很多人写了几年代码,对字符串在内存里的那点事儿,还真不一定清楚。今天我就带大家把这块彻底捋一遍。
4.1 String常量池:JVM的“共享单车”
先问个问题:你写的两个字符串字面量内容一样,它们在内存里是同一份吗?
答案是:不一定。这取决于你怎么创建的。
JVM里有个专门的地方叫String常量池。它的作用说白了就是复用——内容相同的字符串字面量,只存一份。就像共享单车,大家骑的都是同一辆车,没必要每人买一辆。
val s1 = "hello"
val s2 = "hello"
println(s1 === s2) // true,指向同一个对象
但如果你用 new String() 或者 StringBuilder 动态构建,那就另当别论了:
val s3 = String("hello".toCharArray())
println(s1 === s3) // false,不同对象
核心要点:字面量走常量池,动态创建走堆内存。这是最基础的区别。
我在项目中遇到过一个问题:一个接口返回的JSON里有个状态字段,取值就那么几种("SUCCESS"、"FAIL"、"PENDING")。结果每次解析都 new 出新的 String 对象,导致内存里堆了成千上万个重复字符串。后来改成用 intern() 方法手动入池,内存直接降了30%。
小技巧:如果你确定某个字符串会被大量重复使用,可以考虑调用 intern() 把它放进常量池。但注意,频繁调用 intern() 本身也有性能开销,别滥用。
4.2 字符串拼接:+号背后的代价
很多新手喜欢这么写:
val result = ""
for (i in 1..1000) {
result += i.toString()
}
嗯,这段代码看起来挺简单,但性能上简直是灾难。为什么?
因为 String 是不可变的。每次用 += 拼接,都会创建一个新的 String 对象,然后把旧内容复制过去。循环1000次,就创建了1000个临时对象。GC看到这些垃圾,估计都想骂人。
在Java里,编译器会把 + 优化成 StringBuilder。但Kotlin呢?我们来看个例子:
// Kotlin代码
val a = "Hello"
val b = "World"
val c = a + " " + b
反编译成Java后,你会发现Kotlin编译器确实也用了 StringBuilder。但注意,这只对单条语句有效。像上面那个循环,每次迭代都是独立的表达式,编译器没法跨迭代优化。
避坑指南:我曾经在一个日志打印的循环里用了字符串拼接,结果线上出现频繁GC。排查后发现就是这行代码搞的鬼。后来改成 StringBuilder,问题立刻消失。
正确的做法是:
val sb = StringBuilder()
for (i in 1..1000) {
sb.append(i)
}
val result = sb.toString()
或者用Kotlin的 buildString 高阶函数,更优雅:
val result = buildString {
for (i in 1..1000) {
append(i)
}
}
这两种方式都只创建了一个 StringBuilder 对象,性能差距可不是一星半点。
4.3 Kotlin字符串模板:方便但别大意
Kotlin的字符串模板(就是那个 $变量 的写法)确实好用,写起来比Java的 + 拼接舒服多了。但它的内存表现如何?
val name = "Alice"
val age = 30
val msg = "Name: $name, Age: $age"
反编译后你会发现,Kotlin编译器会把模板表达式转换成 StringBuilder 的 append 调用。所以从内存角度看,它和手动写 StringBuilder 是等价的。
但有个坑要注意——模板里的表达式:
val msg = "Result: ${expensiveFunction()}"
每次执行到这行代码,expensiveFunction() 都会被调用一次,然后结果被拼接到字符串里。如果这个函数开销很大,或者这个字符串模板在循环里使用,那性能影响就大了。
我的建议:字符串模板适合简单拼接。如果涉及复杂逻辑或高频调用,先把结果存到变量里,再用模板引用变量。
4.4 实战对比:不同拼接方式的性能
我简单测了一下,在循环10000次的情况下:
| 方式 | 耗时(毫秒) | 创建对象数 |
|---|---|---|
+= 拼接 |
~120 | 10000+ |
| 字符串模板(循环内) | ~85 | ~5000 |
StringBuilder |
~5 | 1 |
buildString |
~5 | 1 |
看到差距了吧?+= 比 StringBuilder 慢了整整20多倍。你想想看,如果你的App里到处都是这种写法,那GC压力得有多大。
4.5 总结一下
关于字符串和常量池,记住这几条就够了:
- 字面量走常量池,动态创建走堆内存。能复用就复用。
- 循环里别用
+=拼接字符串,用StringBuilder或buildString。 - 字符串模板很方便,但注意表达式里的性能开销。
- 如果遇到大量重复字符串,考虑用
intern()入池。
说白了,字符串这块的优化,核心就是减少对象创建。你少创建一个对象,GC就少一次负担,App就流畅一分。这个道理,放之四海而皆准。