"字符串常量池"这个词几乎每个 Java 面试都会问,但很少有人真的用 == 一行行验证过每种写法的真实行为。这篇用 9 组对比真实跑出字面量、new String()、编译期拼接、运行时拼接、intern() 各自的真实结果。
上一篇:Java 泛型擦除真实验证
用 == 比较对象引用,真实观察谁和谁是同一个对象。
StringInternDemo.java String a = "hello"; String b = "hello"; String c = new String("hello"); String d = c.intern();
a == b (两个字面量): true c == a (new String vs 字面量): false c.equals(a): true d == a (c.intern() vs 字面量): true d == c (intern() 返回值 vs new 出来的原对象): false
真实结果很清楚:两个相同内容的字面量 "hello" 编译期就被折进同一个常量池条目,== 真实为 true。new String("hello") 则真实在堆上创建了一个新对象,和常量池里的字面量不是同一个。调用 c.intern() 后,由于常量池里已经有 "hello" 这个条目(是字面量 a 放进去的),intern() 真实返回的就是池里已有的那个引用,和 c 本身(堆上的新对象)不是同一个,但和 a 是同一个。
同样是拼出 "hello",编译期能确定结果的拼接和运行时才能确定结果的拼接,是否进常量池是两回事。
StringInternDemo.java String e = "hel" + "lo"; // 编译期常量拼接 String part = "hel"; String f = part + "lo"; // 运行时变量拼接 String h = new StringBuilder("hel").append("lo").toString();
e == a (编译期常量拼接): true f == a (运行时变量拼接): false g == a (运行时拼接结果 intern 后): true h == a (StringBuilder.toString()): false h.intern() == a: true
真实结果证明了一个常被记混的规则:"hel" + "lo" 里两个操作数都是编译期常量,javac 会在编译期直接把它们折叠成 "hello" 这一个字面量,所以和 a 是同一个对象。但只要拼接里出现了一个变量(part),哪怕它的值在运行期间从未改变,编译器也无法在编译期确定结果,只能真实生成运行时拼接(等价于 StringBuilder),结果对象就落在堆上,和常量池里的字面量不是同一个,必须手动 intern() 才能"合并"回去。
JDK 7 之后字符串常量池搬进了堆,intern() 不再是一个"危险的永久代操作",这里真实测一下量级。
20万次不同字符串 intern() 耗时: 44ms
用 identityHashCode 给每个字符串对象编号,串成动画:编号相同就是常量池里的同一个对象。