我们在开发业务代码判断“相等”常用的方式是" equals","==",两者都可以判断两个值是否相同,但是两者有什么区别呢?看似简单,其实这里面还是有很多坑在里面的,下面这些坑,你踩了多少呢?
equals 和 ==
1."==" 是一个操作符,判断该操作符两端参数是否相等,判等的规则是,两端参数地址值,是否相等。对于基本类型,则是比较两端参数的内容是否相同。
2.equals是Object的一个方法,所有类都会继承这个方法,所以基本类型的比较无法使用equals进行判等。这个方法是用来判断当前对象和方法形参传入对象,之间是否相等。判断的规则默认是 ==,我们在定义类时,可以通过重写这个方法来实现我们自定义的判等规则。
所以如果我们比较两个对象值的内容是否相等时,除了基本类型只能使用 "==" 外,其他类型都需要使用 equals。
Integer 缓存对 == 判等的影响
Integer的缓存,是一种提高程序运行效率和节省jvm内存占用的一种优化手段,默认情况下,Integer缓存[-128,127]范围内的整数,之所以缓存这两个范围内的数字,是通过对大量堆内存分析,发现Integer类型的值大部分都是在-128到127之间。
有了这个缓存后,当我们在用这个范围之内的数字时,就有"可能"会用到已经缓存后的值,而不用在创建新的对象,节省了内存开支,并提升程序运行效率。
为什么说有"可能"会用到呢,通过查看Integer的源码,只有在使用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类的分析,我们会发现 在IntegerCache进行类加载时,会对caches数组进行初始化,初始化数据的范围默认为 -128 到 127。
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;
cache = new Integer[(high - low) + 1];
int j = low;
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() {}
}
同时可以通过对 XX:AutoBoxCacheMax 参数设置来修改 缓存范围的上限,如下面的说明所示:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache { ...}
所以使用 "==" 进行两个Integer对象进行比较时,如果这两个对象 都是缓存值,那么两个对象是相等的,如果两个都不是,或者一个是缓存值,一个不是缓存值,那么这两个对象是不相等的。
Integer 拆箱对 == 判等的影响
使用 == 对基本类型的判等,实际上是使用基本类型的值进行的,那么如果包装类型和一个等值的基本类型进行 == 的判等,结果会是怎样呢?
public static void main(String[] args) {
Integer i = new Integer(1024);
System.out.println(i == 1024);
}
结果会输出 true。
主要原因在于 在执行 "i == 1024" 前,i 会进行拆箱操作,"i == 1024" 表达式, 会变成两个基本类型数字的比较,== 对基本类型的判等,就是对值内容进行比较。不信你可以查看上面两行代码的字节码 :
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/Integer
3: dup
4: sipush 1024
7: invokespecial #3 // Method java/lang/Integer."<init>":(I)V
10: astore_1
11: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_1
15: invokevirtual #5 // Method java/lang/Integer.intValue:()I
18: sipush 1024
21: if_icmpne 28
24: iconst_1
25: goto 29
28: iconst_0
29: invokevirtual #6 // Method java/io/PrintStream.println:(Z)V
32: return
看第15行字节码,在进行判断(21行if_icmpne)前,执行了 Integer.intValue() 进行拆箱操作。
String的常量池,对==的影响
字符串的驻留机制:字符串驻留机制,也就是我们常说的字符串常量池机制,这个机制的初衷是为了节省内存。字符串驻留机制如何触发呢?"当代码中出现双引号形式创建字符串对象时,jvm会对字符串进行检查,如果这个字符串在字符串常量池中的话,那么直接引用常量池中的对象,如果常量池中不存在,就会创建一个新的字符串对象,然后将这个引用对应的字符串存放到字符串常量池中,然后在把这个对象的引用返回。
当我们使用如下方式进行字符串比较的时候:
public static void main(String[] args) {
String s1 = "123";
String s2 = "123";
System.out.println(s1 == s2);
}
基于字符串驻留机制,结果输出是 true
可以通过字节码进行查看:
Constant pool:
#1 = Methodref #8.#31 // java/lang/Object."<init>":()V
#2 = String #32 // 123
常量池中已经包含了字面量"123"
在执行 s1== s2 前会先从常量池中取出 "123" 的引用,然后在进行判等操作,对应的字节码如下:
Code:
stack=3, locals=4, args_size=1
0: ldc #2 // String 123
2: astore_1
3: ldc #2 // String 123
5: astore_2
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: aload_2
11: if_acmpne 18
字节码 0,3行,表示从常量池中取出常量值入栈,然后在11行进行比较
除了在代码中使用双引号形式创建字符串对象,会将 字符串 自动放到常量池中外,还有一种手动将 字符串放到 常量池中的方法, intern(),该方法是String的一个本地方法:
public native String intern();
当我们使用一下代码创建字符串对象时是不会将字符串放到常量池中的,返回结果为 false
private static void internTest(){
String s2= String.valueOf(124);
String s3 = String.valueOf(124);
System.out.println(s2 == s3);
}
但是当我们调用 intern后,会将这个字符串放到常量池中:
private static void internTest(){
String s2= String.valueOf(124).intern();
String s3 = String.valueOf(124).intern();
System.out.println(s2 == s3);
}
结果输出 true。
虽然使用 intern 将字符串放到常量池中,可以减少内存开支,但是,当向 常量池中存放大量的字符串常量时,会导致 intern 方法执行效率下降,原因在于常量池的数据结构是一个map,且这个map的容量大小是固定的,默认值为60013,当向这个map中存放大量数据时会导致每个bucket存放的数据量很大( 字符串常量个数 / 60013 ),当下一次调用intern时,判断将要存放到map中的数据是否存在的过程就会比较耗时。对于这个耗时操作,可以通过空间换时间的方式来避免,通过调整 map的大小,将map中buckets的个数调整一个更大的值,来减少每个bucket中存放的常量个数,加快查询的效率。使用参数 -XX:StringTableSize 来完成对常量池中 bucket 个数的调整。虽然使用 intern可以减少内存创建,但是在使用的过程中还是要控制频度,防止本末倒置,捡了芝麻丢了西瓜。
对于equal判等中经常遇到的坑,下篇文章来讨论吧。