gaarakseven

导航

String a = new String("string")时是否会有 String 实例进入字符串常量池?浅谈字符串对象进入字符串常量池的条件

前言

从了解到字符串常量池那一刻起,就对标题提到的问题很困惑很久。经过一番探索,得到本篇文章。本篇文章是笔者基于 java api层面进行推演得到的答案。
最开始,笔者是想找到在程序运行期间可视化字符串常量池的方式以完成本篇文章,但由于水平有限,对于可视化内存中字符串常量池的内容还没找到一个直观查看的方式。后续能可视化 jvm 内存中字符串常量池之后会在本文补充。
以下内容基于自写的 java 程序和 java api 注释进行推断。

环境

openjdk8 及 openjdk 11

先说结论

在字符串常量池还不存在内容为 “astring” 字符串对象时,新建一个内容为 “astring” 的 String 对象,该对象并不一定会进入字符串常量池,"astring" 也不一定会进入字符串常量池。
只有符合以下条件的、字符串常量池中不存在的字符串对象,才会进入字符串常量池:

  1. 除了 "aa"+"bb" 形式以外,以 "" 包裹的、出现在任何地方的字符串对象的引用都会进入字符串常量池。而"aa" + "bb"形式的表示,最终是"aabb"字符串的引用进入字符串常量池,这是 jvm 优化的结果,后文默认排除该种情况。
  2. 调用了 intern() 的字符串对象。

故,String a = new String("string") 这一行代码确实有 String 实例的引用进入字符串常量池,但不是 a 引用,而是 "string" 字符串对象的引用。

细节探究

关于字符串对象的 == 和 equals

== 判断的是对象的引用是否相等,equals 则是调用对象的 equals 方法进行判断。在 String 类中,equals 判断的是 String 内部用于存储字符串信息的 char 数组的每个元素是否完全相等。

关于 String.intern() 方法

我们先看一下 String.intern() 方法, 这个方法是进行推断的一个关键点. 其英文注释以及译文(笔者水平有限,欢迎在评论区指出错误)如下

/**
Returns a canonical representation for the string object.  
返回字符串对象的规范表示

A pool of strings, initially empty, is maintained privately by the class String.
字符串池由 String 类私有持有, 初始化时为空.(疑问点, 这里说字符串池由 String 类持有, 但在 String 类中没找到相关变量; 难道是指其在 jvm 内存里的 String class 里面存储了字符串常量池的地址?)

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. 
Otherwise, this String object is added to the pool and a reference to this String object is returned.  
调用 intern 方法时(调用方式 astring.intern() ), 如果常量池中存在一个与目标字符串对象 astring 相等的字符串对象 poolString( 判同方式 等价于 poolString.equals(astring)) , 则返回 poolString 的引用; 
否则, 将 astring 对象添加进 字符串常量池并返回 astring 的引用.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
这意味着对于两个两个字符串 s 和 t, 当且仅当 s.equals(t) 为真时, s.intern() == t.intern() 为真.

All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.
所有的文字字符串(这里指的应该是用 双引号 "" 包裹起来的字符串,比如new String("a") 中的 “a”) 以及字符形常量 (个人理解, 这里指的是  String a = "abc" 中的 a)都会具备 intern() 调用后的效果. 
换言之, 前述两种字符串实例直接加入到字符串常量池中,也可以理解为前述两种定义自动调用 intern() 方法.

Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
返回一个与指定字符串内容相同的、来自字符串常量池中唯一的 string 对象.
*/
public native String intern();

关键注释:
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.

所有的文字字符串 ( 这里指的应该是用 双引号 "" 包裹起来的字符串,比如new String("a") 中的 “a”), 以及字符形常量 (个人理解, 这里指的是 String a = "abc" 中的 a. ) 都会具备 intern() 调用后的效果. 也就是, 前述两种字符串实例会进行判断、加入到字符串常量池中,也可以理解为前述两种定义自动调用 intern() 方法

从这一段的注释我们可以推断出:
""包裹的字符串都会进入字符串常量池. 也就是说, String a = new String("string") 这一行代码, 会让 "string"本身作为一个字符串对象进入字符串常量池、而不是 a 实例进入字符串常量池.

此外, 从 intern() 方法的注释我们可以知道, 在执行 intern() 方法时会发生以下事情:
检验目标字符串对象的具体值在字符串常量池中是否存在, 不存在则则将目标对象的引用放入字符串常量池, 并返回目标字符串的引用; 否则返回字符串常量池里与目标字符串对象内容相同的对象引用

验证推断

直接上 demo 代码, 会在注释中说明白。

public static void main(String[] args) {
    String newString = new String("abc");
    String newStringIntern = newString.intern();

    /**
    1. 结果为false。newString 引用并没有被放到字符串常量池里面, 而是 "abc" 字符串对象的引用直接被放到字符串常量池, 
    所以 newString.Intern() 返回的不是 newString 的引用, 而是 "abc" 字符串对象的引用, 故这里的判等为 flase 。
    这里的判等与 (newString.intern() == newString)的写法效果一致
    */
    System.out.println(newStringIntern == newString);



    String stringValueConstant = "abc";
    /**
    2. 结果为true。这里为 true 说明,在执行到 String stringValueConstant = "abc" 时,“
    abc”就已经在字符串常量池中,针对 stringValueConstant 的赋值是直接使用字符串常量池中 “abc” 的引用
    */
    System.out.println(newStringIntern == stringValueConstant);



    String newStringArray = new String(new char[]{'a', 'b'}, 0, 2);
    /**
    3. 结果为true。在这一步中,我们通过传入 char 数组而不是字符串来构造一个 String 对象 newStringArray,
    然后进行 newStringArray.intern()==newStringArray 判断,结果为true。而在第一次判等中, 结果为 false, 
    这证明,字符串对象不是无条件进入字符串常量池、而是有前提的。
    */
    System.out.println(newStringArray.intern() == newStringArray);



    String newStringArray1 = new String(new char[]{'a', 'b'}, 0, 2);
    /**
    4. 结果为  true。这一步的 newStringArray1.intern() 获取的是字符串常量池中已经存在的 “ab”字符串对象,
    也就是第 3 步的 newStringArray---它调用了 intern() 方法,故判等结果为  true。
    */
    System.out.println(newStringArray1.intern() == newStringArray);
    


    String newStringArray2 = new String(new char[]{'c', 'd'}, 0, 2);
    String stringValueConstant1 = "cd";
    /**
    5. 结果为  false。这一步的 newStringArray2 并没有调用 intern() 
    方法、也没有事先定义 "" 类型的字符, 故 newStringArray 没有进入字符串常量池; 而 stringValueConstant1 
    则因为符合条件而进入字符串常量池,  故字符串常量池内存储的是 stringValueConstant1 的引用, 所以判等结果为false 
    */
    System.out.println(newStringArray2 == stringValueConstant1.intern());
}

执行结果如下:

Connected to the target VM, address: '127.0.0.1:6960', transport: 'socket'
false
true
true
true
false
Disconnected from the target VM, address: '127.0.0.1:6960', transport: 'socket'
Process finished with exit code 0

题外话,关于 StringBuilder、StringBuffer

如果有读过周志明的《深入了解 java 虚拟机第三版》的读者,一定对以下代码在 jdk6 和 jdk7 的执行结果有一个印象:

public static void main(String[] args) {
    String str1 = new StringBuilder("计算机").append("软件").toString();
    System.out.println(str1.intern() == str1);//jdk6 为 false; jdk7 为 true
    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2.intern() == str2);//jdk6 为 false; jdk7 为 false
}

笔者是因为这一部分内容的试验发现了一个有趣的现象:
调用 StringBulder("string context") 构造器且调用 apend("context") 方法的 StringBuilder 对象使用 toString() 得到的字符串 str1, str1.intern() == str1的判等结果为 true;
只调用 StringBulder("string context") 构造器而未调用 apend("context") 方法的 StringBuilder 对象使用 toString() 得到的字符串 str2,str2.intern() == str2 的判等结果为 false。

以下为代码(基于openjdk8/11):

public static void main(String[] args) {
    String str1 = new StringBuilder("计算机").append("软件").toString();
    System.out.println(str1.intern() == str1);//true
    
    String str2 = new StringBuilder("计算机").toString();
    System.out.println(str2.intern() == str2);//false
}

基于该现象,笔者进一步探索,从而有了这篇文章。
回到本节,我们先说说str1.intern() == str1 结果为 true 的原因是:

  • StringBuilder 底层用 byte[] 数组存储字符串内容,使用 append 时是针对 byte[] 数组进行更新(确切来说是增量复制),故 "计算机软件" 并没有进入字符串常量池

故 str1.intern() 是将 str1 引用持有的字符串对象放入字符串常量池中并返回 str1 引用,结果自然为 true。

str2.intern() == str2 为 false 的原因:

  • "" 包裹的字符串("计算机")会自动进入字符串常量池
  • StringBuilder.toString() 方法是调用以下 String 的构造器返回 String 对象
    String (byte[] value, byte coder) {
        this.value = value;
        this.coder = coder;}
    }
    

故 str2.intern() 获取到的对象引用(来自字符串常量池)并不是 str2 所持有的引用---str2 引用指向的是 toString 新建的 String 对象。

疑惑点

  • String.intern() 注释有一句话:“A pool of strings, initially empty, is maintained privately by the class String”, 从上下文来看,这里的 “pool of strings” 指的应该就是字符串常量池?这里说的“pool of strings” 由 String 类私有,那么 jvm 是如何保证不同字符串的创建最终都访问到这个常量池呢?另外,我们都知道 class 文件中有一个常量池的数据项,那么 String 类中的常量池和字符串常量池的关系是什么?
  • 关于自动具备 intern 效果的字符串定义,String.intern() 注释中提到:“All literal strings and string-valued constant expressions are interned.”,这里的 literal 指的是用双引号包围起来的字符串定义,那 string-valued constant expressions 呢,是不是指字面量字符串的运算表达式结果,形如 "a" + "b" 的结果 "ab" ?

结语

基于代码进行推断得出的结论可能不一定对,就像对黑盒进行测试并推断黑盒内部的逻辑一样。笔者在寻找切实的能证明黑盒内部逻辑的方式,例如源码阅读或者黑盒内部可视化。
所以笔者最近在尝试将字符串常量池可视化以进一步验证自己的推断,不知道 JHSDB 是否可行。如果有知道相关解决办法的读者,可以在评论区留言指点,感谢!
另,文章内容皆是基于个人理解,欢迎读者在评论区留言讨论和指正。

posted on 2021-02-24 15:14  gaarakseven  阅读(417)  评论(0编辑  收藏  举报