有趣的String和intern()方法

有趣的有趣的String和intern()方法

在JDK1.7及以后的版本里,intern()方法会在常量池中记录首次出现的实例引用,然后返回该引用。

String的声明与常量池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test1 {
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 7 以后,HotSpot已将常量池从永久代转移到了堆中。栈中的引用指向该对象。对于堆中的字符串对象,可以通过 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 Test2 {
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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Constant pool:
#1 = Methodref #15.#37 // java/lang/Object."<init>":()V
#2 = String #27 // baseStr
#3 = String #38 // baseStr01
#4 = Class #39 // java/lang/StringBuilder
#5 = Methodref #4.#37 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#40 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = String #41 // 01
#8 = Methodref #4.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #43 // java/lang/String
#10 = Methodref #9.#44 // java/lang/String."<init>":(Ljava/lang/String;)V
#11 = Methodref #9.#45 // java/lang/String.intern:()Ljava/lang/String;
#12 = Fieldref #46.#47 // java/lang/System.out:Ljava/io/PrintStream;
#13 = Methodref #48.#49 // java/io/PrintStream.println:(Z)V
#14 = Class #50 // StringTest/Test2
#15 = Class #51 // java/lang/Object
...
...
  • 方法栈

介绍两条指令:

  • ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。
  • astore_:Store reference into local variable,将引用赋值给第n个局部变量。
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
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
# 加载常量池中的第二项("baseStr")到栈中。
0: ldc #2 // String baseStr
# 将0中的引用赋值给第一个局部变量,即String baseStr = "baseStr";
2: astore_1
# 加载常量池中的第二项("baseStr")到栈中。
3: ldc #2 // String baseStr
# 将3中的引用赋值给第二个局部变量,即 final String baseFinalStr="baseStr";
5: astore_2
# 加载常量池中的第三项("baseStr01")到栈中。
6: ldc #3 // String baseStr01
# 将6中的引用赋值给第三个局部变量,即String str1="baseStr01";
8: astore_3
# 加载常量池中的第三项("baseStr01")到栈中。
9: ldc #3 // String baseStr01
# 将9中的引用赋值给第四个局部变量:即String str2="baseStr01";
11: astore 4

# 结果#3:str1==str2
# 肯定会返回true,因为str1和str2都指向常量池中的同一引用地址。
# 所以其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。

# 生成StringBuilder的实例。
13: new #4 // class java/lang/StringBuilder
# 复制13生成对象的引用并压入栈中。
16: dup
# 调用常量池中的第五项,即StringBuilder.<init>方法。
# 以上三条指令的作用是生成一个StringBuilder的对象。
17: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V

# 加载第一个参数的值,即"baseStr"
20: aload_1
# 调用StringBuilder对象的append方法。
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# 加载常量池中的第七项("01")到栈中。
24: ldc #7 // String 01
# 调用StringBuilder.append方法。
26: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# 调用StringBuilder.toString方法。
29: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
# 将29中的结果引用赋值改第五个局部变量,即对变量str3的赋值。
32: astore 5
# 结果 #4:因为str3实际上是stringBuilder.append()生成的结果,所以与str1不相等,结果返回false。

# 加载常量池中的第三项("baseStr01")到栈中。
34: ldc #3 // String baseStr01
# 将34中的引用赋值给第六个局部变量,即str4="baseStr01";
36: astore 6
# 结果 #5 :因为str1和str4指向的都是常量池中的第三项,所以str1==str4返回true。
# 这里我们还能发现一个现象,对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。

# 创建String对象
38: new #9 // class java/lang/String
# 复制引用并压如栈中。
41: dup
# 加载常量池中的第三项("baseStr01")到栈中。
42: ldc #3 // String baseStr01
# 调用String."<init>"方法,并传42步骤中的引用作为参数传入该方法。
44: invokespecial #10 // Method java/lang/String."<init>":(Ljava/lang/String;)V
# 调用String.intern方法。
# 从38到41的对应的源码就是new String("baseStr01").intern()。
47: invokevirtual #11 // Method java/lang/String.intern:()Ljava/lang/String;
# 将47步返回的结果赋值给变量7,即str5指向baseStr01在常量池中的位置。
50: astore 7
# 结果 #6 因为str5和str1都指向的都是常量池中的同一个字符串,所以str1==str5返回true。
...
...
  • 对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。所以final String 字段等同于常量赋值。
  • 对于+ 操作符,编译器将其转化为 StringBuilder.append来实现字符串的拼接。

JDK1.6 和JDK1.7的区别

1
2
3
4
5
6
7
8
9
public class InternTest {
public static void main(String[] args) {

String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);//#7
}
}
1
2
3
4
5
6
7
8
public class InternTest01 {
public static void main(String[] args) {
String str1 = "str01";
String str2 = new String("str")+new String("01");
str2.intern();
System.out.println(str2 == str1);//#8
}
}
  • output
    • JDK1.6
      • false
      • false
    • JDK1.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方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。所以:

  • 结果 #7:在第一种情况下,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用,而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此str1和str2都指向堆中的字符串,返回true。

  • 结果 #8:调换位置以后,因为在进行字面量赋值(String str1 = “str01”)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。

-------------本文结束感谢您的阅读-------------
坚持分享,您的支持将鼓励我继续创作!
0%