Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println(a == b); // false
System.out.println(c == d); // true
这段代码第一次看到,确实有点膈应。
同样是 Integer,同样是两个一模一样的数字:
1000 == 1000 -> false
100 == 100 -> true
难不成 Java 觉得 100 比 1000 更相等?
当然不是。
这种问题我一般不在 == 上绕,先盯住一个地方:
Integer a = 100;
右边明明是个 int,左边却是 Integer。
中间肯定有人动了手脚。
这个动作就是自动装箱。
编译器看到:
Integer count = 100;
实际走的效果相当于:
Integer count = Integer.valueOf(100);
注意,不是:
new Integer(100);
问题基本就藏在 Integer.valueOf() 这里。
把它的逻辑缩一下,大概可以理解成这样:
static Integer box(int value){
if (value >= -128 && value <= 127) {
return SMALL_NUMBERS[value + 128];
}
return new Integer(value);
}
这不是让你自己实现一个 Integer,只是把关键逻辑拎出来看。
Java 对一小段常用整数做了缓存。
默认至少包括:
-128 ~ 127
所以执行:
Integer c = 100;
Integer d = 100;
背后相当于两次去缓存里拿 100 对应的那个 Integer 对象。
可以把现场想成这样:
c ─────┐
├──> Integer(100)
d ─────┘
两个引用指向同一个对象。
这时候:
c == d
当然就是:
true
再看 1000。
Integer a = 1000;
Integer b = 1000;
1000 默认已经跑出这段缓存范围了。
两次装箱通常会各自得到一个 Integer 对象:
a ─────> Integer(1000)
b ─────> Integer(1000)
里面的数字一样。
对象不是同一个。
于是:
a == b
结果就是:
false
到这里还得补一刀,因为真正容易把人坑进去的是 ==。
== 到底比较了什么?
如果两边都是基本类型:
int x = 1000;
int y = 1000;
System.out.println(x == y);
结果毫无悬念:
true
因为 int 比的是数值。
但换成:
Integer x = 1000;
Integer y = 1000;
Integer 是对象。
两个引用之间使用:
x == y
比较的是它们是不是指向同一个对象。
不是比较对象里面那个数字是不是相等。
这也是为什么下面这段代码完全不需要考虑什么 127:
Integer x = new Integer(100);
Integer y = new Integer(100);
System.out.println(x == y);
结果还是:
false
因为你已经明确 new 了两个对象。
一个 new 一块对象,另一个 new 又一块对象。
缓存根本没机会插手。
反过来:
System.out.println(x.equals(y));
才是:
true
Integer.equals() 比较的是它保存的整数值。
所以业务代码里看到这种东西,我一般会多看一眼:
Integer statusA = getStatusA();
Integer statusB = getStatusB();
if (statusA == statusB) {
// ...
}
这种写法有个很讨厌的地方。
测试数据如果刚好是:
1
2
10
100
它可能一直跑得好好的。
哪天业务值变成:
200
1000
10000
逻辑突然不进去了。
代码一行没改。
数据一变,Bug 出来了。
这种问题排查起来很容易往业务条件上跑,实际上就是拿对象引用做了比较。
对于 Integer 这种包装类型,我更愿意直接写:
if (Objects.equals(statusA, statusB)) {
// ...
}
而不是:
if (statusA == statusB) {
// 别这么比 Integer
}
Objects.equals() 还有个实际好处,能顺手兜住 null。
比如:
Integer expected = null;
Integer actual = 100;
System.out.println(Objects.equals(expected, actual));
不会因为你直接调:
expected.equals(actual)
再额外送一个 NullPointerException。
还有一种写法也经常让人判断错:
Integer x = 1000;
System.out.println(x == 1000);
这个结果是:
true
看到这里别又怀疑缓存失效了。
这里两边类型不一样:
Integer
int
比较时 x 会被自动拆箱,大致变成:
x.intValue() == 1000
最后比的是两个 int。
自然是 true。
所以这几组代码最好放一起看:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false
System.out.println(c.equals(d)); // true
System.out.println(c == 1000); // true
看起来四行结果乱七八糟,其实背后就两件事:
Integer 装箱时会对一部分小整数复用对象;
== 碰到两个对象引用时,比较的是对象身份,不是里面保存的整数值。
我平时不会去背“100 为什么行,1000 为什么不行”这种题。
看到:
Integer
Long
Short
Character
Boolean
再看到:
==
第一反应就该是先把自动装箱、拆箱和对象比较捋清楚。
至于 Integer 的缓存范围,记住默认至少有:
-128 ~ 127
就够用了。
业务代码更重要的一条是:
Integer a = ...;
Integer b = ...;
Objects.equals(a, b);
别拿 == 赌缓存。
这种代码现在没出问题,很多时候只是因为你的数据还比较小。