写点什么

Java 字符串池、常量池、intern 的爱恨纠葛

发布于: 2021 年 06 月 11 日
Java字符串池、常量池、intern的爱恨纠葛

前言

逛知乎遇到一个刚学 Java 就会接触的字符串比较问题:



通常,根据"==比较的是地址,equals 比较的是值"介个定理就能得到结果。但是 String 有些特殊,通过 new String(string)生成的两个同值的字符串地址就不相等,用其他方式来生成的两个同值字符串地址就相等。


代码如下:


  // 第一种方式创建字符串,字面量赋值  String str1 = "abc";  String str2 = "abc";  // 第二种方式创建字符串    String str3 = new String("xyz");    String str4 = new String("xyz");    System.out.println(str1 == str2); //true    System.out.println(str3 == str4); //false
复制代码


同样是创建字符串,两对等值的字符串进行为什么结果不一样,这就涉及到了常量池和堆。


第一种方式创建的字符串,会将"abc"这个字面量放到了常量池中,然后 str1 和 str2 都指向常量池中的"abc",所以两个变量地址相同;第二种方式创建的字符串,是先在常量池中放入"xyz",然后通过构造函数将常量池中的"xyz"拷贝一份到堆中生成新的 String,和常量池中的"xyx"就没有了关系,所以两个变量指向的是堆中两个不同的变量,所以两个变量地址不同。


那 intern()又是啥?和常量池之间又有什么联系?

常量池

常量池是存放字面量、符号引用或直接引用的地方。而常量池又分为 class 常量池和运行时常量池。

class 常量池

class 常量池是存放编译期类中的字面量和符号引用。上面的字符串"abc"就是字面量;符号引用就是类和接口的完全限定名,字段的名称和描述符,方法的名称和描述符。


如图:



图中的就是 new String(String)这个方法在常量池中的名称和描述符,即符号引用。

运行时常量池

我们平时说的常量池指的就是运行时常量池。在类加载的解析阶段,会将 class 常量池载入内存中(JDK1.7 之前位于方法区,现在位于 Heap 中),并且将符号引用解析成直接引用,即根据对方法/类的描述信息指向内存中对应的方法/类。运行时常量池具有动态性,可以在运行期添加新的变量进入常量池。

intern()

先看一下 intern()这个方法的描述:



用二级英文水平翻译一波,大意就是一个 string 调用 intern()的时候,如果池中有和这个字符串值相等的字符串对象,就会将字符串池中的字符串对象返回;如果没有,就将这个字符串添加进去,并返回这个字符串的引用。字符串池由 String 类私有维护。


这里又引入了字符串池这个概念。

字符串池

字符串池存放的是常量池中字符串对象的引用,而不是字符串对象。通过第一种字面量赋值法创建的字符串会放在常量池中,字符串池就会存储这个字符串对象的引用,当再次在常量池创建字符串时,会先从字符串池查看是否有此字符串的等值引用,如果有的话,直接指向此引用对应的对象。


而第二种方式创建的字符串,会在字符串池中查找是否有与构造参数等值的字符串,以此决定是否需要在常量池新建字符串,然后拷贝常量池中字符串在 Heap 创建一个新的字符串。



如图,在堆中会在常量池中创建一个名为 original 的新字符串,然后拷贝并在堆中生成一个新字符串。注释中也提到,除非你需要一个字符串的显式副本,否则不需要使用这个构造函数,因为字符串是不可变的。


这里使用 intern()测试一下字符串池:


    public static void main(String[] args) {        //第一部分 测试        String str1 = "abc";        String str2 = new String("abc");        System.out.println(str1.intern() == str1); //true        System.out.println(str1.intern() == str2); //false        System.out.println(str1.intern() == str2.intern()); //true        //第二部分 测试通过char[]创建字符串后,引用是否会进入字符串池        String str3 = new String(new char[]{'g', 'h'});        String str4 = "gh";        System.out.println(str3.intern() == str3); //false        System.out.println(str3.intern() == str4); //true        //第三部分 测试char[]创建的字符串调用intern()后引用是否进入字符串池        String str3 = new String(new char[]{'g', 'h'});        str3.intern();        String str4 = "gh";        System.out.println(str3.intern() == str3); //true        System.out.println(str3.intern() == str4); //true    }
复制代码


以上三部分代码是独立测试。


第一部分:str1 在常量池创建了 abc,并将引用放入字符串池,str2 拷贝常量池中的 abc 并在堆中创建新字符串。intern()从字符串池中获取的是常量池中 str1 的 abc 引用。


第二部分:str3 通过 char[]在堆中创建了字符串,不是在常量池,所以 gh 的引用不会自动放入字符串池。str4 在常量池创建了 gh,所以字符串池中保存了 str4 的 gh 引用。intern()从字符串池中获取的是常量池中 str4 的 gh 引用。


第三部分:str3 通过 char[]在堆中创建了字符串,不是在常量池,所以 gh 的引用不会自动放入字符串池,但是它调用 intern()手动将 str3 的 gh 的引用添加到了字符串池中。当 str4 使用字面量赋值创建时,查询到字符串池中有 gh 的引用,str4 就指向了 str3 的 gh 引用。intern()从字符串池中获取的是堆中 str3 的 gh 引用。


从上面的代码中也得出结论:​intern()可以将堆中创建的且字符串池没有等值引用的字符串引用放入字符串池。


同时,这也能说明 String 为什么不可变这个问题。因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,其中的一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不可以的。

言归正传

回到知乎上的问题。在常量池创建了"string"并将其引用放入字符串池,str1 调用 intern()返回的是常量池中的引用,而 str1 指向的是堆中的引用,所以输出为 false。


而 StringBuilder 的 toString()是通过 char[]创建字符串:



在堆中创建了 abcdef 之后,str2 调用 intern()将堆中引用放入字符串池并返回此引用,与 str2 指向堆中同一个字符串对象,所以输出为 true。

结语

Java 中有时候很小的问题也会发散出很多知识点,不论是底层还是 JVM 的理论学习,结合应用案例会理解的更加深刻。就像文中提到的常量池就是 class 文件结构和类加载理论学习的一部分。


文章会在公众号 [入门到放弃之路] 首发,期待你的关注。



发布于: 2021 年 06 月 11 日阅读数: 11
用户头像

公众号:入门到放弃之路 2021.05.23 加入

公众号:入门到放弃之路。自学Java、python、大数据。

评论

发布
暂无评论
Java字符串池、常量池、intern的爱恨纠葛