当前位置: 首页 > 工具软件 > Intern > 使用案例 >

字符串intern()方法详解

贺宏富
2023-12-01

1. intern()方法简介

如果字符串s在字符串常量池中存在对应字面量,则intern()方法返回该字面量的地址;如果不存在,则创建一个对应的字面量,并返回该字面量的地址

2. String对象与字面量的intern()区别

    public static void main(String[] args) {
        String s1 = new String("字符串");
        String s2 = "字符串";
        System.out.println(s2 == s2.intern());
        System.out.println(s1 == s1.intern());
        System.out.println(s1.intern() == s2.intern());
    }

结果是True / False / True,解释如下:

  • 对于字符串字面量s2而言,它本身就是字符串常量池中"字符串"常量的引用,因此s2.intern()返回的是字符串常量池中“字符串”常量的地址,与s2本身是相等的,所以为true
  • 对于String对象s1而言,它是一个指向堆空间String对象的引用。String对象中保存着一个final byte[]用于存储字符串的value,该成员又指向了字符串常量池中“字符串”这个字面量。因此调用s1.intern(),返回的是字符串常量池中"字符串"字面量的地址。s1本身存的是堆空间String对象的地址,因此二者不相等
  • 不管是String对象,还是字面量,只要他们的值相等,调用intern()都会返回同一个字符串常量池的引用,因此s1.intern() == s2.intern()

3. 字符串加法与intern()

    public static void test04(){
        String s1 = "今天吃" + "炒饭";
        String s2 = new String("今天吃") + new String("炒饭");
        String s3 = new String("今天吃") + "炒饭";
        System.out.println(s1.intern() == s1);
        System.out.println(s2 == s2.intern());
        System.out.println(s3 == s3.intern());
    }
    //对应的.class字节码文件
    public static void test04() {
        String s1 = "今天吃炒饭";
        String var10000 = new String("今天吃");
        String s2 = var10000 + new String("炒饭");
        var10000 = new String("今天吃");
        String s3 = var10000 + "炒饭";
        System.out.println(s1.intern() == s1);
        System.out.println(s2 == s2.intern());
        System.out.println(s3 == s3.intern());
    }

返回结果为true / false / false,解析如下:

如果字符串加法中全是字面量相加,则在前端编译为.class字节码文件时,会被直接优化为加和,例如s1在.class文件中被优化为:s1 = "今天吃炒饭",因此s1在被后端编译时,编译器/解释器是看不到相加的过程的,它等效于直接对s1以字面量赋值,所以s1 == s1.intern()

如果字符串加法中存在String对象,前端编译不会将其优化为字面量。关于这个过程,有视频/博客说用的是StringBuilder来做的相加,即new StringBuilder("今天吃").append("炒饭").toString()完成的调用。但是查看字节码文件后,发现新版的JDK不是使用此方式完成的。JDK15中,字符串相加的字节码指令为: 

 invokedynamic #61 <makeConcatWithConstants, BootstrapMethods #0>

它调用了一个makeConcatWithConstants的方法,此方法位于StringConcatFactory类中,JDK对该方法的描述为:

     This strategy replicates what StringBuilders are doing: it builds the
     * byte[] array on its own and passes that byte[] array to String
     * constructor. This strategy requires access to some private APIs in JDK,
     * most notably, the private String constructor that accepts byte[] arrays
     * without copying.

即底层是:拼接得到一个byte[]数组,然后传递给一个String的构造函数得到拼接结果。重点在于其构造函数的参数是byte[],而不是String。因此单独看下方这一行代码,是不会出现"今天吃炒饭"这个字符串的,故字符串常量池中不会出现"今天吃炒饭"这个字面量。则调用s2.intern()方法时,会将s2的地址拷贝一份放到常量池中,然后返回s2的地址,故s2 == s2.intern()。

String s2 = new String("今天吃") + new String("炒饭");

但是这跟我们的结果不一样,结果中得到的是false,这个一个小坑。因为s1中我们令s1 = "今天吃" + "炒饭",已经拼出了一个"今天吃炒饭"字面量,这才导致s1.intern()已经在常量池中得到了字面量。如果我们去掉第一行:

    public static void test04(){
        String s2 = new String("今天吃") + new String("炒饭");
        System.out.println(s2 == s2.intern());
    }

得到的结果就是true

4. StringBuilder与intern()

StringBuilder中维护着一个可变的byte[]用于存储数据,其append(String s)方法是为byte[]数组添加上一些数据,并更新byte[]的有效长度。因此在append过程中,是不会有任何新的字符串被产生的。仅当StringBuilder对象调用toString方法时,才会返回一个new出来的字符串。因此append()过程中,不会有任何中间值出现,例如String s1 = "a".append("b").append("c"),是不会出现"ab"这个字符串的,因为只有调用toString()方法后,才会真正得到字符串对象,其余都是byte[]数组

因此考察如下代码:

    public void test01(){
        String s1 = new StringBuffer("计算机").append("软件").toString();
        System.out.println(s1==s1.intern());
        System.out.println("计算机软件" == s1);
    }

打印true / true,解析如下:

  • 首先出现了"计算机"和"软件"两个字符串常量,常量池中肯定存在二者。new StringBuffer .append()方法调用后,byte[]数组中存储着"计算机软件"的byte[]形式,此时字符串常量池中没有"计算机软件"这个字面量。调用toString()方法后,用byte[]作为参数传入构造器,也没有出现"计算机软件"这个字面量。即第一行代码执行结束后,s1中存储的是堆空间String的地址,且常量池中没有“计算机软件”这个字面量。
  • 当调用s1.intern()后,由于字符串常量池和s1对象都在堆空间中,为了节省空间,不会再将"计算机软件"这个字面量copy一份到常量池中,而是将s1的地址拷贝一份,作为intern()方法的返回值。因此s1 == s1.intern()。此后再使用"计算机软件"这个字面量时,自动返回常量池中的地址,即s1的值,因此"计算机软件" == s1也为true

而对于以下代码:

        String s1 = new StringBuilder("字符串").toString();
        System.out.println(s1.intern() == s1);
        System.out.println(s1 == "字符串".intern());

得到的结果为false / false,原因如下:

在new StringBuilder的过程中,使用到了"字符串"这个字面量,因此new StringBuilder("字符串")已经在常量池中创建了"字符串"字面量。调用toString方法返回堆空间的地址。由于"字符串"字面量已经存在于常量池中,故s1.intern()返回其地址;而s1为堆空间中String对象的地址,二者显然不同。第二个判断也同理

 类似资料: