极简工具箱

免费在线工具 · 本地计算 · 无需上传

Java String 为什么不可变:== 与 equals 的区别

`String a = "abc"; String b = "abc"; a == b` 是 `true`,但 `new String("abc") == "abc"` 是 `false`。搞不清常量池,就会在比较字符串时踩坑。

广告位(百度联盟)

一、String 是不可变的

String s = "abc";
s.concat("def");        // 返回新对象,s 仍是 "abc"
s = s + "def";          // 重新赋值,s 指向新对象

String 内部是 final char[](Java 9 之后是 byte[] + 编码标记),且不提供修改内容的方法。所有「修改」操作都返回新对象。

不可变带来三个好处:

代价是频繁拼接会产生大量临时对象。

二、字符串常量池

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)

几个坑:

七、字符串与编码

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 要指定编码。

本文相关工具(本地计算,无需上传)

更多使用教程