一、String 是不可变的
String s = "abc";
s.concat("def"); // 返回新对象,s 仍是 "abc"
s = s + "def"; // 重新赋值,s 指向新对象
String 内部是 final char[](Java 9 之后是 byte[] + 编码标记),且不提供修改内容的方法。所有「修改」操作都返回新对象。
不可变带来三个好处:
- 可以安全共享:常量池里同一个字面量只有一份。
- hashCode 可缓存:作为
HashMap的键非常高效。 - 线程安全:多线程读不需要同步。
代价是频繁拼接会产生大量临时对象。
二、字符串常量池
String a = "abc"; // 放入常量池
String b = "abc"; // 复用池中的对象
String c = new String("abc"); // 堆上新对象
a == b // true
a == c // false
a.equals(c) // true
== 比较的是引用地址,equals 比较的是内容。字符串比较内容必须用 equals。
new String("abc") 会创建(或复用)池中的 "abc",再在堆上复制一份,所以是两个对象。
三、编译期拼接会被优化
String s = "a" + "b" + "c"; // 编译期直接变成 "abc",进常量池
String x = "a";
String y = x + "b"; // 运行时拼接,结果是堆上新对象(JDK 9+ 用 invokedynamic)
所以 "a"+"b" == "ab" 是 true,而用变量拼出来的不相等。这也是「为什么有时 == 恰好能用」的原因——依赖常量池的巧合,不能作为写法依据。
四、循环里不要用 + 拼接
// 差:每次循环创建新 String,O(n²)
String s = "";
for (String item : list) s += item;
// 好:O(n)
StringBuilder sb = new StringBuilder();
for (String item : list) sb.append(item);
String s = sb.toString();
编译器会为单个表达式里的拼接做优化,但跨循环的拼接优化不了——每轮都新建对象并复制已有内容。
StringBuilder 与 StringBuffer:后者所有方法加 synchronized,线程安全但更慢。方法内部的局部拼接用 StringBuilder 即可,不需要线程安全。
预估长度时可以 new StringBuilder(capacity) 减少扩容。
五、intern()
String s = new String("abc").intern();
s == "abc"; // true
intern() 把字符串放入常量池并返回池中的引用。适合「大量重复字符串」的场景(如解析日志里的字段名)来省内存。但它会操作全局常量池,滥用可能导致池膨胀或 GC 压力,现代 JVM 上收益不一定明显,非必要不用。
六、常用方法与坑
s.length() // 长度(UTF-16 码元数,emoji 会是 2)
s.isEmpty() / s.isBlank() // 空串 / 只含空白(Java 11+)
s.trim() // 去首尾空白(ASCII 空白)
s.strip() // 去首尾空白(Unicode 空白,推荐)
s.substring(1, 3) // 左闭右开
s.split(",") // 正则分割,注意特殊字符
s.replace("a", "b") // 全部替换(字面量)
s.replaceAll("\\d+", "#") // 正则替换
s.equalsIgnoreCase("ABC")
String.join("-", list)
String.format("%s=%d", k, v)
几个坑:
split(".")会得到空数组,因为.是正则通配符,要写split("\\.")。substring在 JDK 7 之后会复制字符数组,不再有「持有原数组导致内存泄漏」的问题。length()是 UTF-16 码元数,emoji 算 2;要按码点计数用codePointCount。equals的参数为null时返回false,但调用方是null会 NPE。常量在前可避免:"abc".equals(s)。
七、字符串与编码
String 内部是 Unicode,getBytes() 不带参数会用平台默认编码,跨平台不一致:
byte[] b = s.getBytes(StandardCharsets.UTF_8); // 明确指定
String s2 = new String(b, StandardCharsets.UTF_8);
文件、网络、数据库交互全部显式指定 UTF-8,能避免绝大多数乱码问题。
小结
String 不可变、字面量进常量池;比较内容用 equals,== 比的是地址;循环拼接用 StringBuilder;split 的参数是正则;getBytes 要指定编码。