字符串拼接原理以及字符串常见面试题
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");
如果有大佬发现不正确的地方,欢迎指正,我会第一时间修改。
以上是关于字符串拼接原理以及字符串常见面试题的主要内容,如果未能解决你的问题,请参考以下文章