Java-String和intern方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Test { public static void main(String[] args){ String str1 = "string"; String str2 = new String("string"); String str3 = str2.intern();
System.out.println(str1==str2); System.out.println(str1==str3);
} }
|
分析
字符串不属于基本类型,但是可以像基本类型一样,直接通过字面量赋值,当然也可以通过new来生成一个字符串对象。不过通过字面量赋值的方式和new的方式生成字符串有本质的区别:
![new String]()
- 通过字面量赋值创建字符串时,会优先在常量池中查找是否已经存在相同的字符串,倘若已经存在,栈中的引用直接指向该字符串;倘若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
- 而通过new的方式创建字符串时,就直接在堆中生成一个字符串的对象(JDK 1.7 以后,HotSpot已将常量池从永久代转移到了堆中。栈中的引用指向该对象。对于堆中的字符串对象,可以通过 intern() 方法来将字符串添加到常量池中,并返回指向该常量的引用。
- 在JDK1.7及以后的版本里,intern方法会在常量池中记录首次出现的实例引用,然后返回该引用。
- 因为str1指向的是字符串中的常量,str2是在堆中生成的对象,所以str1==str2返回false。
- str2调用intern方法,会将str2中值(“string”)复制到常量池中,但是常量池中已经存在该字符串(即str1指向的字符串),所以直接返回该字符串的引用,因此str1==str2返回true。
“final”修饰符 与 “+”操作符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class JavaString { public static void main(String[] args) { String baseStr = "baseStr"; final String baseFinalStr = "baseStr";
String str1 = "baseStr01"; String str2 = "baseStr"+"01"; String str3 = baseStr + "01"; String str4 = baseFinalStr+"01"; String str5 = new String("baseStr01").intern();
System.out.println(str1 == str2); System.out.println(str1 == str3); System.out.println(str1 == str4); System.out.println(str1 == str5); } }
|
反编译分析
1 2 3
| $ ls JavaString.class $ javap -verbose JavaString
|
常量池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| Constant pool: #1 = Class #2 #2 = Utf8 info/victorchu/jvm/j8/JavaString #3 = Class #4 #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref #3.#9 #9 = NameAndType #5:#6 #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Linfo/victorchu/jvm/j8/JavaString; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = String #17 #17 = Utf8 baseStr #18 = String #19 #19 = Utf8 baseStr01 #20 = Class #21 #21 = Utf8 java/lang/StringBuilder #22 = Methodref #23.#25 #23 = Class #24 #24 = Utf8 java/lang/String #25 = NameAndType #26:#27 #26 = Utf8 valueOf #27 = Utf8 (Ljava/lang/Object;)Ljava/lang/String; #28 = Methodref #20.#29 #29 = NameAndType #5:#30 #30 = Utf8 (Ljava/lang/String;)V #31 = String #32 #32 = Utf8 01 #33 = Methodref #20.#34 #34 = NameAndType #35:#36 #35 = Utf8 append #36 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #37 = Methodref #20.#38 #38 = NameAndType #39:#40 #39 = Utf8 toString #40 = Utf8 ()Ljava/lang/String; #41 = Methodref #23.#29 #42 = Methodref #23.#43 #43 = NameAndType #44:#40 #44 = Utf8 intern #45 = Fieldref #46.#48 #46 = Class #47 #47 = Utf8 java/lang/System #48 = NameAndType #49:#50 #49 = Utf8 out #50 = Utf8 Ljava/io/PrintStream; #51 = Methodref #52.#54 #52 = Class #53 #53 = Utf8 java/io/PrintStream #54 = NameAndType #55:#56 #55 = Utf8 println #56 = Utf8 (Z)V #57 = Utf8 args #58 = Utf8 [Ljava/lang/String; #59 = Utf8 Ljava/lang/String; #60 = Utf8 baseFinalStr #61 = Utf8 str1 #62 = Utf8 str2 #63 = Utf8 str3 #64 = Utf8 str4 #65 = Utf8 str5 #66 = Utf8 StackMapTable #67 = Class #58 #68 = Utf8 SourceFile #69 = Utf8 JavaString.java
|
在类加载阶段,JVM不会立即在堆中创建这些Class文件常量池中的字符串对象实例,并把创建完对象的堆中的引用放到字符串常量池中,而是在类的Resolve阶段执行,并且JVM规范里明确指定Resolve阶段可以是Lazy的。
在JVM规范中Class文件的常量池的类型,有两种东西:
- CONSTANT_Utf8
- CONSTANT_String
后者是String常量的类型,但它并不直接持有String常量的内容,而是只持有一个index,这个index所指定的另一个常量池必须是一个CONSTANT_Utf8类型的常量,这里才真正持有字符串的内容。例如上面的#18
-> #19
介绍一条指令:
- ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。ldc指令就是触发lazy resolve动作的条件。如果该项尚未resolve就resolve它,并返回resolve后的内容。在遇到String类型常量时,resolve的过程如果发现String table(全局字符串常量池)已经有了内容匹配的java.lang.String的引用,则直接返回这个引用,反之,如果没有则会在Java堆里创建一个对应内容的String对象,然后在String table中记录下这个引用,并将这个引用返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=8, args_size=1 # 加载常量池中的第16项("baseStr")到栈中。 0: ldc #16 # 将0中的引用赋值给第一个局部变量,即String baseStr = "baseStr"; 2: astore_1 # 加载常量池中的第16项("baseStr")到栈中。 3: ldc #16 # 将3中的引用赋值给第二个局部变量,即 final String baseFinalStr="baseStr"; 5: astore_2 # 加载常量池中的第18项("baseStr01")到栈中。 6: ldc #18 # 将6中的引用赋值给第三个局部变量,即String str1="baseStr01"; 8: astore_3 # 加载常量池中的第18项("baseStr01")到栈中。 9: ldc #18 # 将9中的引用赋值给第四个局部变量:即String str2="baseStr01"; 11: astore 4 # str1==str2 肯定会返回true,因为str1和str2都指向常量池中的同一引用地址。 # 所以其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。
# 生成StringBuilder的实例。 13: new #20 # 复制13生成对象的引用并压入栈中。 16: dup # 加载第一个局部变量,即"baseStr" 17: aload_1 18: invokestatic #22 # 调用常量池中的第28项,即StringBuilder.<init>方法。 # 生成一个StringBuilder的对象。 21: invokespecial #28 # 加载参数的值,即"01" 24: ldc #31 # 使用append方法拼接"baseStr" 和"01" 26: invokevirtual #33 29: invokevirtual #37 # 将29中的结果引用赋值改第五个局部变量,即对变量str3的赋值。 32: astore 5 # 因为str3实际上是stringBuilder.append()生成的结果,所以与str1不相等,结果返回false。 34: ldc #18 36: astore 6 # 因为str1和str4指向的都是常量池中的第三项,所以str1==str4返回true。 # 这里我们还能发现一个现象,对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。 38: new #23 41: dup 42: ldc #18 44: invokespecial #41 47: invokevirtual #42 50: astore 7 # 因为str5和str1都指向的都是常量池中的同一个字符串,所以str1==str5返回true。 52: getstatic #45 55: aload_3 56: aload 4 58: if_acmpne 65 61: iconst_1 62: goto 66 65: iconst_0 66: invokevirtual #51 69: getstatic #45 72: aload_3 73: aload 5 75: if_acmpne 82 78: iconst_1 79: goto 83 82: iconst_0 83: invokevirtual #51 86: getstatic #45 89: aload_3 90: aload 6 92: if_acmpne 99 95: iconst_1 96: goto 100 99: iconst_0 100: invokevirtual #51 103: getstatic #45 106: aload_3 107: aload 7 109: if_acmpne 116 112: iconst_1 113: goto 117 116: iconst_0 117: invokevirtual #51 120: return
|
- 对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。所以final String 字段等同于常量赋值。
- 对于
+
操作符,编译器将其转化为 StringBuilder.append
来实现字符串的拼接。
JDK1.6 和JDK1.7的区别
1 2 3 4 5 6 7 8 9
| public class InternTest01 { public static void main(String[] args) {
String str2 = new String("str")+new String("01"); str2.intern(); String str1 = "str01"; System.out.println(str2==str1); } }
|
1 2 3 4 5 6 7 8
| public class InternTest02 { public static void main(String[] args) { String str1 = "str01"; String str2 = new String("str")+new String("01"); str2.intern(); System.out.println(str2 == str1); } }
|
jdk |
Test01 |
Test02 |
1.6 |
false |
false |
1.7 |
true |
false |
根据对代码的分析,应该可以很简单得出 JDK 1.6 的结果,因为 str2 和 str1本来就是指向不同的位置,理应返回false。
比较奇怪的问题在于JDK 1.7后,对于第一种情况返回true,但是调换了一下位置返回的结果就变成了false。这个原因主要是从JDK 1.7后,HotSpot 将常量池从永久代移到了元空间,正因为如此,JDK 1.7 后的intern方法在实现上发生了比较大的改变,JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。所以:
结果 #1:在第一种情况下,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用,而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此str1和str2都指向堆中的字符串,返回true。
结果 #2:调换位置以后,因为在进行字面量赋值(String str1 = “str01”)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。