字符串拼接原理以及字符串常见面试题

Posted activestruggle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字符串拼接原理以及字符串常见面试题相关的知识,希望对你有一定的参考价值。

第一种情况

    /*
        * 第一种情况
        * 证明:是否在编译的时候完成拼接
        * */
        String str = "a" + "b";

常量池信息:

查看常量池信息必须通过 javap -v 命令来查看Class文件(java文件编译后的文件)

Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // ab
   #3 = Class              #22            // com/test/StringTest2
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Lcom/test/StringTest2;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               str
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               StringTest2.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = Utf8               ab
  #22 = Utf8               com/test/StringTest2
  #23 = Utf8               java/lang/Object

可以看到 #21 是字符串"ab"且没有单独的 "a" 和 "b",说明String str = "a" + "b" 在编译时期就已经拼接在一块了,效果与 String str = "ab" 相同。

进一步说明,第一种情况只在字符串常量池中创建了一个"ab"对象,没有"a"对象和"b"对象。(类加载过程中Class文件的常量池内容会进入字符串常量池)。

第二种情况

      /*
        * 第二种情况
        * 证明:是否是在编译的时候完成拼接,如果不是,那么是按照什么方式进行的拼接。
        * */
        String str = "a";
        String str1 = "b";
        String str3 = str + str1;

 

常量池信息

Constant pool:
   #1 = Methodref          #9.#27         // java/lang/Object."<init>":()V
   #2 = String             #28            // a
   #3 = String             #29            // b
   #4 = Class              #30            // java/lang/StringBuilder
   #5 = Methodref          #4.#27         // java/lang/StringBuilder."<init>":()V
   #6 = Methodref          #4.#31         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Methodref          #4.#32         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #8 = Class              #33            // com/test/StringTest2
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/test/StringTest2;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               str
  #22 = Utf8               Ljava/lang/String;
  #23 = Utf8               str1
  #24 = Utf8               str3
  #25 = Utf8               SourceFile
  #26 = Utf8               StringTest2.java
  #27 = NameAndType        #10:#11        // "<init>":()V
  #28 = Utf8               a
  #29 = Utf8               b
  #30 = Utf8               java/lang/StringBuilder
  #31 = NameAndType        #35:#36        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #37:#38        // toString:()Ljava/lang/String;
  #33 = Utf8               com/test/StringTest2
  #34 = Utf8               java/lang/Object
  #35 = Utf8               append
  #36 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = Utf8               toString
  #38 = Utf8               ()Ljava/lang/String;

 

从常量池中我们可以看到 #28 和 #29 分别为字符串 "a" 和 "b",并且不存在字符串 "ab",所以可以排除第二种情况是在编译期完成的拼接。

那么不是编译器完成拼接那么是通过什么方式进行拼接的呢?我们可以通过查看反汇编指令,来进一步了解拼接的执行过程。

反汇编指令可以通过 javap -v 或者 java -c 查看Class文件(java编译后的文件)得到。

stack=2, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        13: aload_1
        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: aload_2
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: return

 

反汇编指令详解可以百度,这里不做过多解释

我们可以看到 第6行new了一个StringBuilder对象,之后执行了两次append()方法,把字符串 "a" 和 "b" 拼接到一块,然后通过StringBuilder.toString()方法返回一个String对象,该String 对象就是完成拼接后的对象。

这也意味着,通过这种方式将来在类加载的过程字符串常量池中只会保存 "a" 和 "b" 这两个对象,而没有保存 "ab" 这个对象。

当然从编译角度看,编译时期无法识别变量的具体值的,所以 "ab" 这个字符串自然也不会保存到Class文件的常量池中。

这里再提一点:StringBuilder.toString()方法返回的是一个拼接后的String对象,如果字符串很长的时候,尽量不要多次使用StringBuilder.toString()方法,否则会浪费空间甚至造成内存溢出。 

第三种情况

    /*
        * 第三种情况
        * 与第二种差不多
        * */
        String str = new String("a") + new String("b");

 

常量池:

Constant pool:
   #1 = Methodref          #11.#27        // java/lang/Object."<init>":()V
   #2 = Class              #28            // java/lang/StringBuilder
   #3 = Methodref          #2.#27         // java/lang/StringBuilder."<init>":()V
   #4 = Class              #29            // java/lang/String
   #5 = String             #30            // a
   #6 = Methodref          #4.#31         // java/lang/String."<init>":(Ljava/lang/String;)V
   #7 = Methodref          #2.#32         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = String             #33            // b
   #9 = Methodref          #2.#34         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Class              #35            // com/test/StringTest2
  #11 = Class              #36            // java/lang/Object
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/test/StringTest2;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               str
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               SourceFile
  #26 = Utf8               StringTest2.java
  #27 = NameAndType        #12:#13        // "<init>":()V
  #28 = Utf8               java/lang/StringBuilder
  #29 = Utf8               java/lang/String
  #30 = Utf8               a
  #31 = NameAndType        #12:#37        // "<init>":(Ljava/lang/String;)V
  #32 = NameAndType        #38:#39        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = Utf8               b
  #34 = NameAndType        #40:#41        // toString:()Ljava/lang/String;
  #35 = Utf8               com/test/StringTest2
  #36 = Utf8               java/lang/Object
  #37 = Utf8               (Ljava/lang/String;)V
  #38 = Utf8               append
  #39 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #40 = Utf8               toString
  #41 = Utf8               ()Ljava/lang/String;

 

反汇编

   Code:
      stack=4, locals=2, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String a
        13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #8                  // String b
        25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: return

第三种情况和第二种情况不同点:第三种情况会在堆里面多创建两个对象,其它的都一样

———————————————————————————————————————————————————————————————————————————————————————————————————

下面再介绍一下String类型的intern()方法,在jdk1.8中 intern()方法会先去判断字符串常量池中是否存在想要查找的字符串对象,如果存在则返回字符串常量池中对象的地址;如果不存在则返回当前对象(谁调用这个方法,谁就是当前对象)的引用,并且在字符串常量池中创建指向当前对象的引用。注:jdk 1.6与jdk1.8有所不同,有兴趣的可以了解下jdk1.6的intern()方法。

举个例子:

        String str = new String("ab");
        System.out.println(str.intern()==str);    //false
        String str1 = new String("c") + new String("d");
        System.out.println(str1.intern()==str1);  //true    

 

第一种情况,会同时在字符串常量池和堆空间中各生成一个对象,所以str.intern返回的是字符串常量池中 "ab" 的地址,str返回的是堆空间中 "ab" 的地址。

第二种情况,只会在堆空间中生成一个对象,所以str1.intern()返回的是str1对象即堆空间对象的地址,并且会在字符串常量池中创建一个指向堆空间对象的引用。

那么也就是说,如果创建一个引用指向 "cd",比如 String str2 = "cd",那么str1.intern()==str2 也是成立的,但第一种情况就不成立。

 

 

面试题:

        String str = new String("ab");
        String str1 = new String("ab");
        String str2 = "ab";
        String str3 = "ab";
        System.out.println(str==str1);  //false
        System.out.println(str2==str3);  //true
        System.out.println(str2==str);  //false
        System.out.println(str.intern()==str2); //true
        System.out.println(str.intern()==str1.intern());  //true    
        String str = "a";
        String str1 = "b";
        String str2 = str1 + str;
        String str3 = str2.intern();
        String str4 = "ab";
        System.out.println(str2==str4); //false
        System.out.println(str3==str4); //false 因为java文件中含有"ab"字符串,所以编译成Class文件后,常量池会包含 "ab"

 

     //问:创建几个对象
        //答:最明显的字符串常量池分别创建了 “a”和"b",堆空间创建了 "a"和"b",然后字符串拼接会创建StringBuilder对象,
        // StringBuilder对象会调用toString()方法产生拼接后的String对象,所以总共创建了6个对象
        String str = new String("a") + new String("b");

 

 

如果有大佬发现不正确的地方,欢迎指正,我会第一时间修改。

 

 

 

以上是关于字符串拼接原理以及字符串常见面试题的主要内容,如果未能解决你的问题,请参考以下文章

毕业季--Java框架常见面试题

10 个 MyBatis 常见面试题

js正则表达式常见面试题

python常见面试题讲解字符串最后一个单词的长度

python常见面试题讲解字符串最后一个单词的长度

Mybatis常见面试题(转)