深入理解String与new String

前言

字符串是最常用的类型之一,趁此机会准备探索下它的源码。有关该类的注释作一个总结:

String-字符串,是个常量,它们被创建后其值就不允许被改变,由于它是不可变的,所以它们可以被共享,在内部提供了多个方法来操作字符串。

探索之前我曾看过其他人写的有关于此的文章,发现JDK1.7版本前后的内存模型不一样,而这部分的内容还没有排上行程,简单来说,我还不懂...所以不敢妄下结论,这篇文章的内容也不会从这方面展开来讲,此次探索是基于JDK1.8

原理

字符串是存放在常量池中,至于什么是常量池不准备讨论它。先来个简单的示例方便分析:


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("testOne");
        }
    }

示例就是这么简单...先让我们来看看仅仅是这么一句话,而它的底层都做了什么。紧接着反编译它的字节码文件,命令是javap -v TestString,输出的信息内容比较多,咱们只要常量池的内容,如图所示:

常量池-1

实际上我也看的不是很懂,但有几个点咱们是能确定的,也就是说常量池中有testOne字符串,而此时的字节码文件是编译而来的,并未实际上的运行,所以该字符串对象在堆内存中还未创建,这就说明了在编译阶段时字符串就存在于常量池中,同时也验证了一点:常量池中的字符串对象与运行时在堆内存中创建的字符串对象完全是两个对象,因为在编译时期堆内存中的对象还未出生呢,所以不可能引用到它...不知道我讲明白了没有。接着我们从另外一个角度去分析这段示例,因为后续的其他示例可能会羞涩难懂,所以先从简单的示例开始逐渐熟悉起来,最终也是希望读者能够理解这方面的知识。


    Constant pool:
  
// 编号  类型                值              注释            --------> 这里的注释不一定对,只是作一个标记方便理解        
    #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
    #2 = Class              #23            // java/lang/String
    #3 = String             #18            // testOne
    #4 = Methodref          #2.#24         // java/lang/String."<init>":(Ljava/lang/String;)V
    #5 = Class              #25            // test20190820/TestString
    #6 = Class              #26            // java/lang/Object
    #7 = Utf8               <init>
    #8 = Utf8               ()V
    #9 = Utf8               Code
    #10 = Utf8               LineNumberTable
    #11 = Utf8               LocalVariableTable
    #12 = Utf8               this
    #13 = Utf8               Ltest20190820/TestString;
    #14 = Utf8               main
    #15 = Utf8               ([Ljava/lang/String;)V
    #16 = Utf8               args
    #17 = Utf8               [Ljava/lang/String;
    #18 = Utf8               testOne
    #19 = Utf8               Ljava/lang/String;
    #20 = Utf8               SourceFile
    #21 = Utf8               TestString.java
    #22 = NameAndType        #7:#8          // "<init>":()V
    #23 = Utf8               java/lang/String
    #24 = NameAndType        #7:#27         // "<init>":(Ljava/lang/String;)V
    #25 = Utf8               test20190820/TestString
    #26 = Utf8               java/lang/Object
    #27 = Utf8               (Ljava/lang/String;)V

执行javap -c TestString获取底层代码执行逻辑,如图所示:

常量池-2


    public class test20190820.TestString {
        public test20190820.TestString(); //这里是构造器的执行逻辑,咱们忽略它,毕竟不是重点
            Code:
            0: aload_0
            1: invokespecial #1                  // Method java/lang/Object."<init>":()V
            4: return

    public static void main(java.lang.String[]); //下面的内容涉及到字节码的指令,网上很多资料,可自行查阅
        Code:
        // #2:对应常量池中编号#2,加上注释我们得出这里是创建字符串对象  -> new String();
        0: new           #2                  // class java/lang/String
        // dup:复制0序号中的引用并压入栈中  -> 也就是将字符串对象的引用放入到栈中
        3: dup
        // ldc:从常量池中加载指定项的引用到栈  #3:同上,对应着常量池的编号#3 -> 将"testOne"字符串的引用加载到栈中
        4: ldc           #3                  // String testOne
        // invokespecial:初始化常量池中的指定项  -> 调用字符串对象的初始化并将4序号中的引用作为参数传入,形成new String("testOne")
        6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        // astore_n:将引用赋值给第n个局部变量  -> 将6序号的引用赋值给第一个局部变量,String testOne = new String("testOne"),jvm中是有记录局部变量的信息
        9: astore_1
        // 退出方法的标志
        10: return
    }

从上面的分析可以知道:常量池中的字符串对象与在堆内存中创建的字符串对象是两个对象,这一点很重要!接下来是各种示例,我们将采用上面的方式进行分析。

示例一


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("testOne");
            String testOneAnother = "testOne";
            System.out.println(testOne == testOneAnother); //false -> 上面说了testOne和testOneAnother是两个对象,所以很容易得出结果
        }
    }

示例二


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a";
            String s3 = "bc";
            String s4 = s2 + s3; // StringBuilder.append(a).append(bc) -> StringBuilder.toString() -> new String("abc")
            System.out.println(s1 == s4); //false, s1指向了常量池中的字符串abc,s4相当于生成了新的对象
        }
    }


    /**
     * 这里就不贴常量池的信息了,反正我们能确定的是在编译时期字符串已经加载到常量池中了,所以常量池中应该存在abc、a、bc字符串
     * 贴出代码的执行逻辑:
     *  public static void main(java.lang.String[]);
     *      Code:
     *          0: ldc           #2                  // String abc   ->  ldc:从常量池中加载指定项的引用到栈
     *          2: astore_1                                          ->  abstor_1:将引用赋值给第1个局部变量, 即 s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                                          ->  s2 = "a"
     *          6: ldc           #4                  // String bc
     *          8: astore_3                                          ->  s3 = "bc"
     *          9: new           #5                  // class java/lang/StringBuilder    -> 创建StringBuilder对象
     *          12: dup    ->  复制StringBuilder对象的引用并压入栈中
     *          13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V    -> 初始化StringBuilder对象
     *          16: aload_2                            -> 加载第二个局部变量的值
     *          17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  -> 调用常量池中指定项的方法,即调用StringBuilder对象中的append方法,并传入序号 *                                                                                                                                           16中的引用,所以最终是StringBuilder#append(a)
     *          20: aload_3
     *          21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     *          24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;      -> StringBuilder#toString, 可以看下该类的toString方法,生成了一个新的对象,该对象中的字符串不会再*                                                                                                                        常量池中生成
     *          27: astore        4                    -> 将序号24中的引用赋值给第4个局部变量 s4 = StringBuilder.toString = new String()
     *          29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;   -> 下面的就讨论了,执行的是System.out.println代码片段了
     *          32: aload_1
     *          33: aload         4
     *          35: if_acmpne     42
     *          38: iconst_1
     *          39: goto          43
     *          42: iconst_0
     *          43: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     *          46: return
     *
     * 
     * 从上面的分析中我们得出结论:s1指向常量池中的字符串对象abc,而从序号16-27我们知道生成了新的对象,相当于是执行了new String("abc"),而这不就又回到了示例一了吗,结果自然是false
     */

示例三


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            final String s2 = "a";
            final String s3 = "bc";
            String s4 = s2 + s3; //由于s2、s3被final修饰了,故而直接替换变量的值,最后s4 = "abc",直接使用了常量池中的字符串对象abc
            System.out.println(s1 == s4); //true
        }
    }

    /**
     * 同样贴出代码的执行逻辑:
     *  public static void main(java.lang.String[]);
     *      Code:
     *          0: ldc           #2                  // String abc  -> ldc:从常量池中加载指定项的引用到栈
     *          2: astore_1                                         -> s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                                         -> s2 = "a" 
     *          6: ldc           #4                  // String bc
     *          8: astore_3                                         -> s3 = "bc"
     *          9: ldc           #2                  // String abc
     *          11: astore        4                                 -> s4 = "abc"  示例三与示例二的区别在于加了final修饰,被final修饰的变量会在编译阶段直接替换成对应的值,即"a" + "bc",而这个在示例四中我们也会分析到,是直接采用
     *                                                                             字符串合并,而合并后的字符串abc在常量池中已经存在了,故直接使用
     *          13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          16: aload_1
     *          17: aload         4
     *          19: if_acmpne     26
     *          22: iconst_1
     *          23: goto          27
     *          26: iconst_0
     *          27: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
     *          30: return
     * 从上面的分析中我们得出结论:s1指向了常量池中的字符串对象abc,s4也是指向了常量池中的字符串对象abc
     */


示例四


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a" + "bc"; //s1、s2指向同一个字符串
            String s3 = "test" + "One"; //说明常量池是直接存储合并后的字符串
            System.out.println(s1 == s2); //true
        }
    }

    /**
     * 这里多贴出常量池的信息,为了说明s3的行为
     * Constant pool:
     * ...
     * #30 = Utf8               abc
     * #31 = Utf8               testOne
     * ...
     * 省略了一部分信息,说明常量池是直接存储合并后的字符串,而并分开存储,所以常量池中只会有"testOne",并没有"test"或"One"
     *
     *  public static void main(java.lang.String[]);
     *       Code:
     *          0: ldc           #2                  // String abc
     *          2: astore_1                            -> s1 = "abc" 指向常量池中#2的引用
     *          3: ldc           #2                  // String abc
     *          5: astore_2                            -> s2 = "abc" 从常量池中#2的引用,可以看到引用的字符串对象是同一个
     *          6: ldc           #3                  // String testOne
     *          8: astore_3
     *          9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          12: aload_1
     *          13: aload_2
     *          14: if_acmpne     21
     *          17: iconst_1
     *          18: goto          22
     *          21: iconst_0
     *          22: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
     *          25: return
     * 从上面的分析中我们得出结论:s1、s2指向了常量池中的同一个字符串对象
     */

示例五


    public class TestString {

        public static void main(String[] args) {
            String s1 = "abc";
            String s2 = "a";
            String s3 = s2 + "bc"; // StringBuilder.append(a).append(bc) -> StringBuilder.toString() -> new String("abc")
            System.out.println(s1 == s3);//false
        }
    }
    /**
     * public static void main(java.lang.String[]);
     *       Code:
     *          0: ldc           #2                  // String abc
     *          2: astore_1                             -> s1 = "abc"
     *          3: ldc           #3                  // String a
     *          5: astore_2                             -> s2 = "a"
     *          6: new           #4                  // class java/lang/StringBuilder   -> 创建StringBuilder对象
     *          9: dup           ->  复制StringBuilder对象的引用并压入栈中
     *          10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V   -> 初始化StringBuilder
     *          13: aload_2                             -> 加载第二个局部变量的值
     *          14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   -> StringBuilder.append("a")
     *          17: ldc           #7                  // String bc
     *          19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   -> StringBuilder.append("bc")
     *          22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;    -> StringBuilder.toString
     *          25: astore_3                           -> s3 = "abc"
     *          26: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     *          29: aload_1
     *          30: aload_3
     *          31: if_acmpne     38
     *          34: iconst_1
     *          35: goto          39
     *          38: iconst_0
     *          39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     *          42: return
     * 从上面的分析中我们得出结论:s1指向常量池中的字符串对象abc,而从序号9-25我们知道生成了新的对象,相当于是执行了new String("abc"),结果自然是false
     */


示例六

先来看下String#intern方法作了什么动作,还是采用此分析方式。


    public class TestString {

        public static void main(String[] args) {
            String testOne = new String("a") + new String("bc"); //常量池中存储字符串a、bc,最终testOne指向堆内存中的对象,而该对象对应的字符串是不会在常量池中存在
            String testOneAnother = testOne.intern(); //先去常量池中查询是否已经存在该字符串,如果存在,则返回常量池中的引用,若不存在则不会将该对象的字符串拷贝到常量池中,而是在常量池中持有对该对象的引用
                                                      //这里的引用没办法从该方式得出,可能需要看下native的方法,反正我是看了别人的分析,虽然我是知道原理但还是忍不住看了以下底层实现
            System.out.println(testOne == testOneAnther) //true
        }
    }
    /**
     * Constant pool:
     *  ...
     * #31 = Utf8               a
     *  ...
     * #34 = Utf8               bc
     */


示例七


    public class TestString {

        public static void main(String[] args) {
            String s1 = new String("a") + new String("bc");
            s1.intern(); //执行方法前我们知道常量池中并未有abc字符串,执行该方法后,常量池中已经存在指向s1对象的引用,即"abc"字符串的引用
            String s2 = "abc"; // 常量池中已经存在"abc"字符串的引用,即为s1对象的引用
            System.out.println(s1 == s2);//true
        }
    }
    /**
     * public static void main(java.lang.String[]);
     *      Code:
     *          0: new           #2                  // class java/lang/StringBuilder    -> 创建StringBuilder对象
     *          3: dup                                -> 复制StringBuilder对象的引用并压入栈中
     *          4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V  -> 初始化StringBuilder
     *          7: new           #4                  // class java/lang/String    -> 创建字符串对象 new String()
     *          10: dup                               -> 复制String对象的引用并压入栈中
     *          11: ldc           #5                  // String a
     *          13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V    -> new String("a")
     *          16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; -> StringBuilder.append()
     *          19: new           #4                  // class java/lang/String    ->  创建字符串对象 new String()
     *          22: dup                               -> 复制String对象的引用并压入栈中
     *          23: ldc           #8                  // String bc
     *          25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V   -> new String("bc")
     *          28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; -> StringBuilder.append()
     *          31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;  -> StringBuilder.toString()
     *          34: astore_1                          -> s1 = "abc", 由StringBuilder#toString创建的字符串对象
     *          35: aload_1                           -> 加载第一个局部变量的值
     *          36: invokevirtual #10                 // Method java/lang/String.intern:()Ljava/lang/String;  -> s1.intern()
     *          39: pop                               -> pop:移除栈顶的值
     *          40: ldc           #11                 // String abc
     *          42: astore_2                          -> s2 = "abc"
     *          43: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
     *          46: aload_1
     *          47: aload_2
     *          48: if_acmpne     55
     *          51: iconst_1
     *          52: goto          56
     *          55: iconst_0
     *          56: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
     *          59: return
     */


示例八


    public class TestString {

        public static void main(String[] args) {
            String s1 = new String("abc"); //生成两个对象,堆内存一个,常量池一个
            s1.intern(); //常量池中已经存在该字符串对象,故而直接返回
            String s2 = "abc"; //指向常量池的字符串对象
            System.out.println(s1 == s2);//false  -> s1指向堆内存中的对象,s2指向常量池的对象
        }
    }

总结

  • 在编译阶段,字符串已经被存储与常量池中。

  • new String("abc"):一共有两个不同的对象,一个在堆内存、一个在常量池。

  • s2 + s3拼接(s2、s3未被final修饰):底层创建StringBuilder对象,通过append拼接起来,最终调用toString生成一个新的对象。

  • "a" + "bc"直接拼接:直接将拼接后的字符串存储于常量池中。

  • s2 + "bc"拼接(s2未被final修饰):底层创建StringBuilder对象,通过append拼接起来,最终调用toString生成一个新的对象。

  • s.intern:若常量池中存在字符串,则直接返回引用,若不存在,则在常量池中生成指向该字符串对象的引用,后续若有声明此字符串,会返回指向该字符串对象的引用,也就是同一个引用(参考示例七、八)。

重点

new String与String的区别 (s1 + s2)与("a" + "bc")的区别 intern

posted @ 2020-12-21 19:21  zliawk  阅读(692)  评论(0)    收藏  举报