目录
[一、== 和 equals 的经典陷阱](#一、== 和 equals 的经典陷阱)
[二、intern() 的行为差异](#二、intern() 的行为差异)
[三、String 不可变,但别以为绝对安全](#三、String 不可变,但别以为绝对安全)
[四、substring 的内存泄漏(JDK6)](#四、substring 的内存泄漏(JDK6))
[五、split 的坑](#五、split 的坑)
[七、String.format 的性能与本地化](#七、String.format 的性能与本地化)
[八、switch 对 String 的 NPE](#八、switch 对 String 的 NPE)
[九、String 与 CharSequence 的混淆](#九、String 与 CharSequence 的混淆)
[十、+ 拼接的编译优化边界](#十、+ 拼接的编译优化边界)
[十一、StringBuilder 的线程安全](#十一、StringBuilder 的线程安全)
[十二、isEmpty() 与 isBlank()(JDK11+)](#十二、isEmpty() 与 isBlank()(JDK11+))
[十三、replace 与 replaceAll 的区别](#十三、replace 与 replaceAll 的区别)
[十五、toCharArray() 与不可变性的假象](#十五、toCharArray() 与不可变性的假象)
AI总结的
一、== 和 equals 的经典陷阱
示例代码:
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + "lo";
String e = "hel";
String f = e + "lo";
System.out.println(a == b); // true 常量池同一对象
System.out.println(a == c); // false new 在堆上
System.out.println(a == d); // true 编译期常量折叠
System.out.println(a == f); // false 运行期拼接产生新对象
System.out.println(a == f.intern()); // true
要点:
-
字面量在常量池,
new String()在堆。 -
编译期能确定的拼接(都是字面量)会折叠;只要有一个变量参与,就是运行期
StringBuilder拼接,产生新对象。 -
比较内容永远用
equals,需要忽略大小写用equalsIgnoreCase。
二、intern() 的行为差异
示例代码:
String s = new String("a") + new String("b"); // 堆上 "ab",常量池没有
s.intern();
String t = "ab";
System.out.println(s == t); // JDK6: false, JDK7+: true
JDK6 的 intern() 会把字符串复制 到永久代常量池;JDK7+ 常量池移到堆中,intern() 直接记录堆中对象的引用,所以 s == t 为 true。这个差异在面试和跨版本迁移中经常被问到。
三、String 不可变,但别以为绝对安全
示例代码:
String s = "abc";
s.concat("def"); // 返回值被丢弃,s 仍是 "abc"
s.toUpperCase(); // 同样无效
s = s.concat("def"); // 必须重新赋值
String 的每个"修改"方法都返回新对象。循环里拼接字符串是性能杀手:
示例代码:
// 反例:每次循环创建新 String 对象
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
// 正例
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
四、substring 的内存泄漏(JDK6)
JDK6 中 substring 共享原字符串的 char[]:
示例代码:
String huge = 读取一个100MB的字符串;
String small = huge.substring(0, 2); // small 仍持有整个 100MB 数组
huge = null; // 但 small 还引用着,内存无法回收
JDK7+ 已改为复制出新数组,不再有这个问题。老代码迁移时要注意。
五、split 的坑
示例代码:
"a.b.c".split("."); // 返回空数组!因为 "." 是正则任意字符
"a.b.c".split("\\."); // 正确
"a,b,c,".split(","); // ["a","b","c"] 尾部空串被丢弃
"a,b,c,".split(",", -1); // ["a","b","c",""] 保留尾部空串
"".split(","); // [""] 长度为1
要点:
-
参数是正则,特殊字符要转义。
-
默认丢弃尾部空字符串,用负的 limit 保留。
-
频繁 split 可用
StringUtils.split(Apache Commons)或手动解析,避免正则开销。
六、字符编码问题
示例代码:
byte[] bytes = "中文".getBytes(); // 用平台默认编码,跨平台不一致
byte[] bytes = "中文".getBytes("UTF-8"); // 明确指定
String s = new String(bytes); // 同样用默认编码,可能乱码
String s = new String(bytes, "UTF-8");
要点:
-
凡是
getBytes()/new String(byte[])不指定编码,都是隐患。 -
读文件、网络传输、数据库连接都要明确 charset。
-
String.length()返回的是 UTF-16 code unit 数量,emoji 等增补字符会占 2 个:
示例代码:
"😀".length(); // 2
"😀".codePointCount(0, "😀".length()); // 1
七、String.format 的性能与本地化
示例代码:
String.format("%d", 100); // 内部用 Formatter,性能较差
在热点循环里慎用,可用 StringBuilder 或 Integer.toString。另外 %f 受 Locale 影响,某些地区小数点会变成逗号:
示例代码:
String.format("%.2f", 1.5); // 在德语环境可能是 "1,50"
String.format(Locale.US, "%.2f", 1.5); // 稳定 "1.50"
八、switch 对 String 的 NPE
java
String s = null;
switch (s) { // 直接 NPE
case "a": ...
}
switch 内部调用 s.hashCode(),null 会抛 NPE。先判空。
九、String 与 CharSequence 的混淆
java
CharSequence cs = "abc";
cs.length(); // 有
cs.charAt(0); // 有
cs.equals("abc"); // 可能 false,取决于实现类是否重写
接口方法有限,且 equals 语义不保证。需要字符串语义时先 toString()。
十、+ 拼接的编译优化边界
java
String a = "x" + 1 + 2; // "x12" 编译期折叠
String b = 1 + 2 + "x"; // "3x" 先算 1+2
运算顺序按从左到右,+ 两边只要出现字符串就变成拼接,但前面是数字会先做加法。这个容易看错。
十一、StringBuilder 的线程安全
java
StringBuilder sb = new StringBuilder(); // 非线程安全
StringBuffer sb = new StringBuffer(); // 线程安全,方法加 synchronized
单线程用 StringBuilder,多线程共享才用 StringBuffer(或直接避免共享)。StringBuilder 默认容量 16,频繁扩容影响性能,能预估大小就指定初始容量。
十二、isEmpty() 与 isBlank()(JDK11+)
java
"".isEmpty(); // true
" ".isEmpty(); // false
" ".isBlank(); // true 只含空白
JDK11 之前判断空白要自己 trim,现在用 isBlank() 更清晰。
十三、replace 与 replaceAll 的区别
java
"a.b".replace(".", "-"); // "a-b" 字面量替换
"a.b".replaceAll(".", "-"); // "---" "." 是正则,匹配任意字符
"a.b".replaceAll("\\.", "-"); // "a-b"
replace 是字面量,replaceAll / replaceFirst 是正则。还有 replace(CharSequence, CharSequence) 和 replace(char, char) 两个重载,别混淆。
十四、字符串常量池与内存
大量动态生成的字符串(如拼接出的 key)不会自动进常量池,只有字面量和显式 intern() 才进。滥用 intern() 会导致常量池膨胀甚至 OOM。JDK7+ 常量池在堆里,受 -XX:StringTableSize 影响,哈希冲突多时性能下降。
十五、toCharArray() 与不可变性的假象
java
char[] arr = "abc".toCharArray();
arr[0] = 'x'; // 不影响原字符串,因为返回的是副本
但如果通过反射改 String 内部的 value 数组,就能破坏不可变性(JDK9+ 用 byte[] 且模块系统限制更强)。正常代码不要这么做。
小结
| 坑点 | 核心记忆 |
|---|---|
== vs equals |
内容比较永远用 equals |
| 拼接 | 循环里用 StringBuilder |
split |
参数是正则,注意转义和 limit |
| 编码 | 永远指定 charset |
substring |
JDK6 有内存泄漏,JDK7+ 已修复 |
format |
性能差、受 Locale 影响 |
switch |
对 null 会 NPE |
replaceAll |
是正则,不是字面量 |
掌握这些,日常开发中绝大多数字符串相关的 bug 都能避开。