一篇文章带你彻底弄懂Java的==符号
在Java中==符号的作用分为两类:1:==符号在八种基本类型的作用是比较对应基本类型的数值是否相等2:==符号在对象类型的作用是比较两个对象是否相等在对象类型中又有两类特殊情况,一种是基本类型中包装类对象,一种是String对象。前者由于存在缓存导致,后缀则是有字符串常量池的存在导致。
本篇文章6735字,大概阅读时间20分钟。本文中使用到的JDK版本为1.8.0_301
目录
==符号的定义
在Java中==符号的作用分为两类:
1:==符号在八种基本类型的作用是比较对应基本类型的数值是否相等
2:==符号在对象类型的作用是比较两个对象是否相等
在对象类型中又有两类特殊情况,一种是基本类型中包装类对象,一种是String对象。前者由于存在缓存导致,后缀则是有字符串常量池的存在导致。
基本类型中==符号的判断
在基本类型中==符号的作用是判断基本类型的数值是否相同
int i1 = 1;
int i2 = 2;
int i3 = 1;
System.out.println(i1 == i2); //false
System.out.println(i1 == i3); //true
char c1 = 'a';
char c2 = 'b';
char c3 = 'a';
System.out.println(c1 == c2); //false
System.out.println(c1 == c3); //true
在基本类型的包装类中由于存在缓存以及自动拆箱/装箱,导致==符号的使用存在一定的差异,下面将会解释到。
首先基本类型和包装类对应关系
基本类型 | 包装类型 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
在看一段代码(建议将代码复制粘贴到别处,下文会经常提到这段代码中的内容)
Integer i1 = 127; //数值127 自动装箱
Integer i2 = 127; //数值127 自动装箱
System.out.println(i1 == i2); //true
Integer i3 = 128; //数值128 自动装箱
Integer i4 = 128; //数值128 自动装箱
System.out.println(i3 == i4); //false
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6); //false
Integer i7 = new Integer(128);
Integer i8 = new Integer(128);
System.out.println(i7 == i8); //false
int i9 = 127;
System.out.println(i1 == i9); // true i1自动拆箱
System.out.println(i5 == i9); // true i9自动拆箱
首先i1==i2(true)和i3==i4(false) 这两种情况,要解释这两种情况,先看看自动拆箱/装箱背后的逻辑。将上面这段代码编译成Class文件,然后通过javap命令进行反编译,查看字节码的执行逻辑 。
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息。
通过执行 javap -c /xx.class 命令,输出下面内容
在输出内容中,可以看到反汇编后,代码Integer i1 = 127在=号右边的127会调用Integer.valueOf(127)方法,将数值127包装成Integer(127)的Integer对象,同理i2、i3、i4也是相同原理。通过上述内容可以看出,自动装箱就是调用包装类的valueOf()方法将基本类型的值封装成对应的包装类对象。
现在我们去看一下Integer.valueOf方法具体做了哪些事情
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在看一下IntegerCache的代码
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h; //127
//创建一个Integer类型的cache数组长度为256
cache = new Integer[(high - low) + 1];
int j = low;
//从下标0开始依次存入-128 -127 .... 126 127
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
在valueOf()方法中首先会判断传入的i在不在-128~127之间,如果在这区间就会调用 IntegerCache.cache,而cache是一个Integer对象的数组,并且是在static代码块中进行初始化,在IntegerCache对象初始化完成后,cache数组已经初始化完毕。所以如果在-128~127之间,直接返回cache中已经创建好的Integer对象,如果不在-128~127之间则会重新new一个Integer对象。
通过上面的分析就可以得出 i1=127和i2=127在进行自动装箱的时候,通过valueOf方法返回的都是同一个已经在cache中存在的Integer(127)对象,而i3=128和i4=128则是直接通过new的方式创建的两个不同的Integer对象。而==符号在比较两个对象的时候是判断两个对象是否相等,明显i1和i2都是cache缓存中的同一个对象所以==比较结果为true,而i3和i4是两个不同的对象,所以==比较的结果是false。
注意:通过上面的分析,在包装类比较数值是否相等的时候需要使用equals方法进行,不要使用==进行比较数值是否相等,由于有缓存的存在可能导致结果出现差异。
继续看上面的代码,在 i5、i6、i7、i8都是创建的不同的Integer对象,所以使用==符号的时候比较的结果都是false。
在i9处定义了一个数值为127的常量,i1==i9和i5==i9比较的时候,我们继续查看反编译 在i1==i9和i5==i9比较的时候 i1和i5会调用intValue()方法,获取Integer对象中存储的常量值
public int intValue() {return value;}
所以i1==i9和i5==i9比较的是数值大小,在基本类型和包装类型进行比较的时候,包装类型通过调用xxxValue()方法获取对应的常量值,这个就是自动拆箱。
总结:
1:在引用类型是包装类型的时候,而等号右边是基本类型的字面量,类似Integer i = 127的时候,会触发自动装箱,调用对应的valueOf()方法将字面量封装成包装类型。
2:在基本类型的字面量和包装类对象比较的时候,包装类对象会调用对应xxxValue()方法,获取对象中的字面量值,所以比较的是字面量的值。
3:基本类型的自动装箱就是调用对应包装类的valueOf()方法,而自动拆箱就是包装类调用xxxValue()方法。
上面介绍的时候是使用int和int的包装类Integer作为例子,下面将会罗列出八种基本类型全部情况。
基本类型 | 包装类型 | 缓存(valueOf方法) |
byte | Byte | 存在缓存 -128~127之间 |
short | Short | 存在缓存 -128~127之间 |
int | Integer | 存在缓存 -128~127之间 |
long | Long | 存在缓存 -128~127之间 |
float | Float | 没有缓存 |
double | Double | 没有缓存 |
char | Character | 存在缓存 0~127之间 |
boolean | Boolean | 存在缓存 true/false |
String类型中==符号的判断
本文所用的JDK版本为1.8.0_301,在不同JDK版本中字符串常量池存在差异,所以接下来代码的运行环境都是1.8.0_301版本。
在JDK8中字符串常量池的定义为:字符串常量池在堆中存储
先看下面代码
String s1 = "abc";
String s2 = new String("abc");
通过javap命令进行反编译
上图中ldc、astore这些JVM指令的解释可以查看这里,使用页面搜索来查找
解读反编译后的内容
Code 0:ldc指令将"abc"字面量存储到字符串常量池中,也就是String s1 = "abc"。如果常量池中不存在则需要在常量池中创建,如果常量池中有则直接使用。
Code 2:将s1引用保存到局部变量表中
Code 3:new一个字符串对象,对应的是String s2 = new String("abc")
Code 6:将创建的对象s2压入操作数栈中
Code 7:由于s2中的字符串"abc"已经存在在字符串常量池中,所以不需要再次创建"abc",直接从常量池中获取
Code 9:初始化s2对象,调用String(str String)的构造方法,将已经存在常量池中的"abc"字符串传入构造方法,完成s2对象的创建
Code 12:将s2引用保存到局部变量表中
再将上面一段代码,顺序颠倒
String s2 = new String("abc");
String s1 = "abc";
再通过javap命令进行反编译
解读反编译后的内容
Code 0:new一个字符串对象,对应的是String s2 = new String("abc")
Code 3:将创建的对象s2压入操作数栈中
Code 4:ldc指令将new String("abc")中的"abc"字面量存储到字符串常量池中,如果常量池中不存在则需要在常量池中创建,如果常量池中有则直接使用。这里"abc"由于在常量池中不存在,则第一次创建。
Code 6:初始化s2对象,调用String(str String)的构造方法,将已经存在常量池中的"abc"字符串传入构造方法,完成s2对象的创建
Code 9:将s2引用保存到局部变量表中
Code 10:创建String s1 = "abc",由于"abc"已经在常量池中存在,则无需再次创建,直接使用常量池中的"abc"。
Code 12:将创建的对象s1压入操作数栈中
通过上面的分析,总结如下:
1:jdk8中字符串常量池存是在堆中创建一块区域进行存储。
2:字符串字面值的创建是直接在字符串常量池中进行创建,在创建前会检查常量池中是否已经存在对应的字符串,如果不存在则在常量池中进行创建,存在则直接使用。
3:如果通过new 来创建字符串对象,首先会判断字面值是否在常量池中已经存在,如果不存在则在常量池中进行创建,存在则直接使用。然后将常量池中的字符串引用传递给在堆中创建的字符串对象,再将堆中创建的字符串对象传递给s2。
通过上面的分析还可得出一个常见的面试问题String s2 = new String("abc"),s2的创建过程一共创建了几次对象?
答案是一次或者两次
一次:类似第一种情况,常量池中已经存在了对应的"abc"字符串,所以只需要在堆中创建new String("abc")对象即可,然后将堆中的对象指向s2。
两次:类似第二种情况,常量池中不存在,则首先要在常量池中创建一次"abc"字符串对象,然后再在堆中第二次创建new String("abc")对象,并且把常量池中的对象指向堆中的对象,然后堆中的对象再指向s2。
通过上面的分析,接下来可以研究==符号在字符串中的判断
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //false
System.out.println(s3 == s4); //false
s1和s2都是在常量池中创建的,并且在s2创建的时候"abc"字符串已经存在,所以s2得引用指向的是s1,故s1==s2为true,而s3和s4都是在堆上创建的对象,虽然s3和s4中字符串指向的是常量池中的同一个"abc",但是s3和s4是堆上的不同对象所以 s1==s3和s3==s4为false
下面在分析几种使用+拼接字符串的情况下==符号的判断
在判断之前先熟悉两个概念
常量折叠优化:是指Java在编译期做的一个优化,将一些表达式在编译期能计算好的,不用放到运行期间进行计算
例如 1+1或者 "ab" +"c"这些可以在编译期直接计算出表达式的结果 2和"abc"。
字面量:指直接写在代码中的数字、字符串或布尔值。例如数值123 123.11或者字符串"abc"直接在代码中定义的值。
接下来看+号拼接字符串的情况下==符号的判断
第一种情况:常量折叠优化
String s1 = "abc";
String s2 = "a" + "b" + "c";
System.out.println(s1 == s2); //true
在这种情况下 s2在编译期间会进行常量折叠优化,直接拼接成"abc"。
第二种情况:拼接字符串中有一个是变量或是通过new
关键字实例化的对象
String s1 = "abc";
String s2 = "ab";
String s3 = s2 + "c";
String s4 = new String("ab") + "c";
String s5 = new String("ab") + new String("c");
System.out.println(s1 == s3); //false
System.out.println(s1 == s4); //false
System.out.println(s1 == s5); //false
这种情况下,由于无法再编译器确定对象的值,只能在运行期间确定,s3、s4、s5都是在堆上创建的不同的字符串对象,所以比较结果都是false。
第三种情况:final
修饰的字面量实例化的字符串编译器在编译前也可以直接确认它的值。
String s1 = "abc";
final String s2 = "ab";
String s3 = s2 + "c";
String s4 = s2 + new String("c");
System.out.println(s1 == s3); //true
System.out.println(s1 == s4); //false
s2是被final修饰的常量,其值不会发生改变。在s3=s2+"c",可以看成 s3="ab"+"c"。所以在上面的判断中s1==s3为true。 而s4中虽然s2是常量带时候+后面跟着一个变量,所以在堆中创建了s4,故s1==s4为false。
通过上面的分析,我们可以得出如下结论:
1:字符串的字面量都是存在在常量池中,并且如果字面量在常量池中已经存在,则不会再次创建。
2: 由于有常量折叠优化的存在,字符串的字面量在使用+号拼接的时候可以在编译期确定其值
3:+号拼接字符串中有一个是变量或是通过new
关键字实例化的对象,都是会在堆中创建其对象
4:final
修饰的字面量实例化的字符串编译器在编译前也可以直接确认它的值。
还有在字符串需要比较值是否相等的时候,需要使用equals()方法。并且equals方法的左边最好是确定不为空或者字符串的字面值。防止出现NullPointException异常。
结束语:
本文从八大基本类型和其包装类,以及String对象分析==字符在Java中的应用判断情况,相信通过本篇文章的阅读,可以使你如同文章标题一样,全面搞定==符号。当然这也是我写这篇文章的目的,感谢您能看到最后。
方便自己,也方便他人,坚持原创!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)