【Java】字符串相关坑点

目录

[一、== 和 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 都能避开。

相关推荐
Terra.K2 小时前
Spring AI day1(SSE+AI)
java·人工智能·spring·springai
用户019027581612 小时前
如何用 Python 统计 A 股连续涨停(几连板)?
python
斑鸠喳喳2 小时前
线程本地存储 ThreadLocal
java·后端
外收内放2 小时前
Python基础语法练习题(拓展1)
python·游戏
jimy12 小时前
虚函数vtable,链接时看到构造函数,linker把符号地址写入vtable(三)
开发语言·c++
H_unique2 小时前
Chat2Excel:接口自动化测试
python·测试工具·自动化
ss2732 小时前
AI全栈实战 | 3.3-02 Python 并发:有 GIL 为什么还用多线程,asyncio 和 JS 事件循环同源不同味
开发语言·javascript·python
菜鸟~noob2332 小时前
【电子战】第20篇:凸优化与对数障碍法【含matlab代码】
开发语言·matlab
空心木偶☜2 小时前
Langgraph操作时常见的错误
python·ai·ai编程·langgraph