Post

运行时StringBuilder动态拼接的String是否会被自动放入字符串常量池中?

基本的八股文概念就不赘述了,本文主要讲解自己踩的坑

其实直接用逻辑推断也能得出答案:不会 ,否则设计这么个常量池的意义何在。
但是我因为学艺不精,很多概念没有理解透彻,然后多饶了很久去验证想法。本篇博文就是分享我的思路历程。

写在最前

本篇博文主要记录我自己的心路历程,所以一开始我也记录了我的一些错误的想法,其直到后面的部分才被矫正,所以推荐先看完这部分

运行时创建的任何String对象都不会自动进入常量池。字符串加入到字符串常量池的时间点只有两个

  • 在编译器就可以确定的字符串常量会在类加载后先存储到运行时常量池
    接着在代码运行到对应部分的时候,具体来说是执行字节码文件中的ldc指令时,在字符串常量池中创建String对象 (一些在编译器就可以确定的常量拼接操作会直接优化计算出结果,此时则只创建拼接后的对象)
  • 运行时创建的String要调用intern()方法后才会放入字符串常量池。注意放入这个操作不会移动这个堆中的String对象,而是仅将引用赋值给字符串常量池维护的StringTable。

然后是几个注意点:

  • intern()方法会返回已经在字符串常量池的值相等的字符串引用,无则直接将调用方String存入常量池。
    (这就是为什么不能用intern与自己做==比较操作来判断String是否已经存入常量池的原因)
  • 有一些偏门的面试题目(所以才称作八股文)会问字符串常量池中存储的是String对象还是其引用,问题的答案如下 java的字符串常量池在堆中的元空间(metaspace),编译器确定的字符串常量最终会创建在这里, 其维护一个了一个StringTable保存String常量的引用。所以作为堆空间的一部分的常量池即保存对象也保存引用,而具体来说,其维护的StringTable只保存引用。

起因

思考标题中的这个问题时,使用了如下代码进行验证

1
2
String abcd = new StringBuilder().append("ab").append("cd").toString();
System.out.println(abcd.intern() == abcd);

结果返回为True,我便以为动态创建的String会被自动存入字符串常量池中。

但仔细想想又不对劲,这样的设计意义何在? 但是我又想到 其实我这种写法就是String abcd = "ab" + "cd" 经过编译器优化后的结果,那么最终abcd会被存储字符串常量池中也确实符合逻辑。 但是有一个反例出现了,我把代码中的字符串字面值换成变量,而返回结果仍然是ture,这又让我无法解释。

坑1:一个对intern()的错误用法

我显然是想要验证,一个字符串是否在字符串常量池中。 但我实际做的事情是:查看intern()方法的返回值是否等于字符串本身。 这两者真的等价吗?

intern()是用来干什么的?

intern()方法会返回字符串对象在常量池中对应的版本的引用。 即如果常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;否则,将此String对象包含的字符串添加到常量池中,再返回此String对象的引用。

那我用intern()方法来验证字符串是否在字符串常量池中,有什么问题呢?

问题就在于调用intern()方法后,字符串必定被加入到字符串常量池中。 我们已经将字符串放入常量池了,此时再去验证字符串是否在常量池中,显然是没有意义的。具体来说,对于一个从未在字符串常量池中出现过的字符串x ,x.intern() == x 是一个值为ture的恒等式

坑2:一种对字符串常量池机制的错误想象

intern()的方法说明中,当参数没有在字符串常量池中出现过时,会将参数放入字符串常量池中。

一个字符串变量调用intern()后如何放入字符串常量池中

我原来的理解是创建一个新的Clone的String对象,然后将其放入字符串常量池中。 但实际上放入的行为就是将intern()调用方String的引用存储到字符串常量池中。

那么一些字符串字面量(常量)是怎么放入字符串常量池中的

在编译后,类加载器会检查class文件中的字符串常量,然后创建后放入字符串常量池中。
具体来说,若涉及到字符串拼接,会自动进行优化,不保存中间结果,只保存最终结果。
(已更新,查看最开始的写在前面部分的内容)

补充,一个流行的面试题

String s1 = new String(“abc”);这句话创建了几个字符串对象?
答:会创建 1 或 2 个字符串对象。若字符串常量池中已经存在”abc”,则只会创建一个字符串对象;否则,会创建两个字符串对象。

其实更完善的说法是,这个语句不论如何会且只会创建一个堆中的对象, ~~ ~~因为常量池中的对象的创建时机如上一点所说,是在类加载时就完成了(已更新)

(更新)

这道面试题确实是这么回答的

在JDK 7及以上版本中,Java字符串常量池被移到了堆上,也就是说字符串常量池不再是在类加载时初始化,而是在运行时对字符串对象进行初始化并存储。而这个具体的初始化时间点可能因为JVM实现而有所不同,但一般都是懒加载。

具体来说,在 HotSpot 的实现中,任何常量值在类加载后就存储在元空间中的运行时常量池中了,然后在执行有字符串常量部分的代码时,才会在堆中的字符串常量池中初始化该String对象然后返回该对象在常量池中的引用。更具体地说,这个懒加载的具体加载的时机就是运行到对应字节码中对应位置的ldc指令时进行操作,可以参考这里 查看字节码中的ldc指令。

回顾一些具有误导性的例子,以及正确的理解

一个简单的例子

1
2
3
4
5
String a = "a";
System.out.println(a.intern() == a); // true

String _a = new String("a");
System.out.println( _a.intern() == _a); // false

正确的理解:首先不能用intern()来判断字符串是否在字符串常量池中,但我们先看下去。首先,"a"是一个字面值,所以它会一开始就被放入字符串常量池中。 继续分析 a的值是"a"在常量池中的引用, _a的值是新创建在堆中的一个对象的引用。因为"a" 已经在常量池中存在,所以a.intern() 返回"a"在常量池中的引用,_a.intern()返回的也是"a"在常量池中的引用。 这就解释了为什么第一个是true,第二个是false。

解释最开始的例子

1
2
String abcd = new StringBuilder().append("ab").append("cd").toString();
System.out.println(abcd.intern() == abcd); // true

首先,"ab""cd"一开始就在常量池中了(这里不确定,但不影响后面的推理), 接着Builder返回value为"abcd" 的String对象abcd,而常量池中此时还没有值为"abcd"的对象,所以当执行abcd.intern()时,会将abcd 的引用放入字符串常量池中,然后返回那么最终的结果就是自己和自己比,就是true

如果这样写呢?

1
2
3
4
5
String abcd = "abcd";

String abcd2 = new StringBuilder().append("abc").append("d").toString();
System.out.println(abcd2.intern() == abcd2);

结果就是false,因为这里的"abcd"是字面值,所以它一开始就在常量池中了, 而abcd2是一个新创建的对象,执行abcd2.intern() 时,返回的引用是abcd,所以结果为false

小结

  • 判断字符串是否在常量池中,直接和字面值比较而不是用 intern() 方法的返回值和自己比较,因为字面值可以保证在一开始类加载的时候就存入常量池中了,而intern()方法必定将字符串放入常量池中,所以不具有判断的意义。
  • (通过StringBuilder拼接或其他方式例如substring等)动态创建的String不会被加入到字符串常量池中,除非显式的调用intern() 方法,这时候才会将其加入到字符串常量池中。
  • 动态创建的String调用intern()方法后,会将其引用放入字符串常量池中,而不是创建一个新的对象的克隆放入pool。

对第二点的补充:我直接用构造器创建的String对象算是动态(运行时)创建的吧,但是为什么还是存在常量池中去了呢?

例如 new String (“abc”) 其实这是因为这个语句在编译后,class文件中还是出现了字面值”abc”,所以在类加载时,会将其放入常量池中。如果你这么写 new String (“某变量”),那么这个变量的值是在运行时才确定的,所以这个语句在编译后,class文件中就不会出现字面值”某变量” ,那么进一步在类加载时,就不会将其放入常量池中。

具体再举个例子

1
2
3
4
5
6
7
8
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

注意str4就是真的动态创建的新的String,而str3和str5其实是一回事,在编译时就已经确定了。所以str3和str5在常量池中,而str4不在常量池中。

This post is licensed under CC BY 4.0 by the author.