一、发生的现状:
public static void main(String[] args){
String a="abc";
final String b="abc";
String c="abc1";
System.out.println(a+1==b+1); //false
System.out.println(a+1==c); //false
System.out.println(b+1==c); //true
}
上述这个代码块,运行结果两个是false,一个是true!那么为什么会产生这个现象呢?
二、原因说明:
以下内容,我当做各位读者是有相应基础的:第一、“==”知道是比较的什么吧;第二、final 知道是什么吧;第三、分得清字符串池(常量池)、堆空间、栈帧、局部变量表、操作数栈和反汇编(javap指令)。
如果以上说的3点不清楚,那么,请百度查看下相应的资料吧。
首先javap -v 查看反汇编内容:(在这里进行一下简单的注释解释,只复制了main中反汇编内容);
这里面关于final的核心代码是序号: 79;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
//除了前面说的是操作数栈,后面注释直接用的是栈来代替
0: ldc #2 //ldc是将int, float或String型常量值从常量池中推送至操作数栈顶 // String abc
2: astore_1 //这个其实就是把"abc"从操作数栈弹出,赋值给变量a(局部变量表1的位子变量)
3: ldc #2 //这里的#2可以不用管,动态链接的东西 // String abc
5: astore_2 //同理赋值给变量b,这个数字2自行研究去
6: ldc #3 // String abc1
8: astore_3 //同理赋值给变量c
9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
12: new #5 //创建一个StringBuilder对象,并将其引用值(地址)压入栈顶 // class java/lang/StringBuilder
15: dup //复制栈顶数值并将复制值压入栈顶
16: invokespecial #6 //根据编译时类型来调用实例方法 // Method java/lang/StringBuilder."<init>":()V
19: aload_1 //读取位子1的数据,也就是a变量引用地址的数据推到栈顶
20: invokevirtual #7 //调用append方法把"abc"添加入StringBuilder中 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: iconst_1 //将int型1推送至栈顶
24: invokevirtual #8 //append上面那个1 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
27: invokevirtual #9 //调用StringBuilder的toString方法 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: ldc #3//这个点参考下方序号79的解释 // String abc1
32: if_acmpne 39//比较栈顶两引用型数值,当结果不相等时跳转
35: iconst_1 //把int类型常量1压入栈
36: goto 40//跳转到40的序号那行
39: iconst_0 //把int类型常量0压入栈,实际上这里被goto跳过了,下面是重复的
40: invokevirtual #10// Method java/io/PrintStream.println:(Z)V
43: getstatic #4// Field java/lang/System.out:Ljava/io/PrintStream;
46: new #5// class java/lang/StringBuilder
49: dup
50: invokespecial #6// Method java/lang/StringBuilder."<init>":()V
53: aload_1
54: invokevirtual #7// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
57: iconst_1
58: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
61: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
64: aload_3 //直接拿到c变量地址,指向字符串常量池
65: if_acmpne 72
68: iconst_1
69: goto 73
72: iconst_0
73: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
76: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
//以上代码,对应是java代码的第二个打印结束 System.out.println(a+1==c);
//这里,我们就来看看 如果有final将会有什么区别
79: ldc #3//这句是关键,final修饰的b变量,没有了转StringBuilder的动作, 直接是从常量池中拿的abc1,因为c变量的存在,常量池已经存在了abc1; // String abc1
81: aload_3 //装载局部变量3 也就是c变量 abc1
82: if_acmpne 89 //比较栈顶两引用型数值,当结果不相等时跳转
85: iconst_1
86: goto 90
89: iconst_0
90: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
93: return
三、结论说明:
- 字符串池最初是空的,由String类私有地维护(可以参考String的intern()方法的解释)。
- 字符串底层的计算是使用的StringBuilder(线程不安全)进行拼接的。所以,最后的值是StringBuilder的toString方法转出来的,该方法为new String;其地址并未指向自字符串常量池中。而final修饰的字符串,它值的引用,一直是指向字符串常量池!
- a+1==b+1//false a+1是StringBuilder的toString,底层代码如下;
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
所以实际a+1等价于new String(“abc1”)。常量池中如果没有abc1,则在new的String对象操作中,会先把abc1放入池中,再返回该值的引用,而栈中变量,则指向的是new String在堆中的地址,new String内部的char数组则指向的常量池地址。而b+1则是直接指向了地址中已经存在的abc1字段的地址;
a+1== c// c直接指向字符串常量池地址 a+1如上所述
b+1== c //b+1是直接指向字符串常量池中"abc1"的地址,c也是指向该地址,所以为true