Java-String和intern方法

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);//#1
System.out.println(str1==str3);//#2

}
}
/**
* -------- output ----------
* false
* true
*/

分析

字符串不属于基本类型,但是可以像基本类型一样,直接通过字面量赋值,当然也可以通过new来生成一个字符串对象。不过通过字面量赋值的方式和new的方式生成字符串有本质的区别:

new String

  • 通过字面量赋值创建字符串时,会优先在常量池中查找是否已经存在相同的字符串,倘若已经存在,栈中的引用直接指向该字符串;倘若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
  • 而通过new的方式创建字符串时,就直接在堆中生成一个字符串的对象(JDK 1.7 以后,HotSpot已将常量池从永久代转移到了堆中。栈中的引用指向该对象。对于堆中的字符串对象,可以通过 intern() 方法来将字符串添加到常量池中,并返回指向该常量的引用。
  • 在JDK1.7及以后的版本里,intern方法会在常量池中记录首次出现的实例引用,然后返回该引用。
  1. 因为str1指向的是字符串中的常量,str2是在堆中生成的对象,所以str1==str2返回false。
  2. 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);//#3
System.out.println(str1 == str3);//#4
System.out.println(str1 == str4);//#5
System.out.println(str1 == str5);//#6
}
}
/**
* -------- output ----------
* true
* false
* true
* true
*/

反编译分析

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 // info/victorchu/jvm/j8/JavaString
#2 = Utf8 info/victorchu/jvm/j8/JavaString
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#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 // baseStr
#17 = Utf8 baseStr
#18 = String #19 // baseStr01
#19 = Utf8 baseStr01
#20 = Class #21 // java/lang/StringBuilder
#21 = Utf8 java/lang/StringBuilder
#22 = Methodref #23.#25 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#23 = Class #24 // java/lang/String
#24 = Utf8 java/lang/String
#25 = NameAndType #26:#27 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#26 = Utf8 valueOf
#27 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
#28 = Methodref #20.#29 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#29 = NameAndType #5:#30 // "<init>":(Ljava/lang/String;)V
#30 = Utf8 (Ljava/lang/String;)V
#31 = String #32 // 01
#32 = Utf8 01
#33 = Methodref #20.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#34 = NameAndType #35:#36 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = Utf8 append
#36 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = Methodref #20.#38 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#38 = NameAndType #39:#40 // toString:()Ljava/lang/String;
#39 = Utf8 toString
#40 = Utf8 ()Ljava/lang/String;
#41 = Methodref #23.#29 // java/lang/String."<init>":(Ljava/lang/String;)V
#42 = Methodref #23.#43 // java/lang/String.intern:()Ljava/lang/String;
#43 = NameAndType #44:#40 // intern:()Ljava/lang/String;
#44 = Utf8 intern
#45 = Fieldref #46.#48 // java/lang/System.out:Ljava/io/PrintStream;
#46 = Class #47 // java/lang/System
#47 = Utf8 java/lang/System
#48 = NameAndType #49:#50 // out:Ljava/io/PrintStream;
#49 = Utf8 out
#50 = Utf8 Ljava/io/PrintStream;
#51 = Methodref #52.#54 // java/io/PrintStream.println:(Z)V
#52 = Class #53 // java/io/PrintStream
#53 = Utf8 java/io/PrintStream
#54 = NameAndType #55:#56 // println:(Z)V
#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 // "[Ljava/lang/String;"
#68 = Utf8 SourceFile
#69 = Utf8 JavaString.java

在类加载阶段,JVM不会立即在堆中创建这些Class文件常量池中的字符串对象实例,并把创建完对象的堆中的引用放到字符串常量池中,而是在类的Resolve阶段执行,并且JVM规范里明确指定Resolve阶段可以是Lazy的。

在JVM规范中Class文件的常量池的类型,有两种东西:

  1. CONSTANT_Utf8
  2. 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 // String baseStr
# 将0中的引用赋值给第一个局部变量,即String baseStr = "baseStr"
2: astore_1
# 加载常量池中的第16项("baseStr")到栈中。
3: ldc #16 // String baseStr
# 将3中的引用赋值给第二个局部变量,即 final String baseFinalStr="baseStr"
5: astore_2
# 加载常量池中的第18项("baseStr01")到栈中。
6: ldc #18 // String baseStr01
# 将6中的引用赋值给第三个局部变量,即String str1="baseStr01";
8: astore_3
# 加载常量池中的第18项("baseStr01")到栈中。
9: ldc #18 // String baseStr01
# 将9中的引用赋值给第四个局部变量:即String str2="baseStr01"
11: astore 4
# str1==str2 肯定会返回true,因为str1和str2都指向常量池中的同一引用地址。
# 所以其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。

# 生成StringBuilder的实例。
13: new #20 // class java/lang/StringBuilder
# 复制13生成对象的引用并压入栈中。
16: dup
# 加载第一个局部变量,即"baseStr"
17: aload_1
18: invokestatic #22 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
# 调用常量池中的第28项,即StringBuilder.<init>方法。
# 生成一个StringBuilder的对象。
21: invokespecial #28 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
# 加载参数的值,即"01"
24: ldc #31 // String 01
# 使用append方法拼接"baseStr""01"
26: invokevirtual #33 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #37 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
# 将29中的结果引用赋值改第五个局部变量,即对变量str3的赋值。
32: astore 5
# 因为str3实际上是stringBuilder.append()生成的结果,所以与str1不相等,结果返回false
34: ldc #18 // String baseStr01
36: astore 6
# 因为str1和str4指向的都是常量池中的第三项,所以str1==str4返回true
# 这里我们还能发现一个现象,对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。
38: new #23 // class java/lang/String
41: dup
42: ldc #18 // String baseStr01
44: invokespecial #41 // Method java/lang/String."<init>":(Ljava/lang/String;)V
47: invokevirtual #42 // Method java/lang/String.intern:()Ljava/lang/String;
50: astore 7
# 因为str5和str1都指向的都是常量池中的同一个字符串,所以str1==str5返回true
52: getstatic #45 // Field java/lang/System.out:Ljava/io/PrintStream;
55: aload_3
56: aload 4
58: if_acmpne 65
61: iconst_1
62: goto 66
65: iconst_0
66: invokevirtual #51 // Method java/io/PrintStream.println:(Z)V
69: getstatic #45 // Field java/lang/System.out:Ljava/io/PrintStream;
72: aload_3
73: aload 5
75: if_acmpne 82
78: iconst_1
79: goto 83
82: iconst_0
83: invokevirtual #51 // Method java/io/PrintStream.println:(Z)V
86: getstatic #45 // Field java/lang/System.out:Ljava/io/PrintStream;
89: aload_3
90: aload 6
92: if_acmpne 99
95: iconst_1
96: goto 100
99: iconst_0
100: invokevirtual #51 // Method java/io/PrintStream.println:(Z)V
103: getstatic #45 // Field java/lang/System.out:Ljava/io/PrintStream;
106: aload_3
107: aload 7
109: if_acmpne 116
112: iconst_1
113: goto 117
116: iconst_0
117: invokevirtual #51 // Method java/io/PrintStream.println:(Z)V
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
}
}
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);//#2
}
}
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。