AI摘要
作者因用==比较Integer导致线上积分系统误更新,踩坑Integer缓存范围(-128~127),悟出包装类、String必须用equals,并总结Code Review清单防重蹈覆辙。
(一) 开场白:那个让我记忆犹新的周五夜晚
“差不多了,提测,下班!”我心里美滋滋地想着。那是去年一个普通的周五,我完成了一个用户积分变动的需求。自测通过,完美。刚走出公司大楼,手机响了,是企业微信的报警群消息:“线上用户积分流水记录异常增多,请相关开发人员紧急排查。”
我的心里“咯噔”一下。用手机连上VPN,查看ELK日志,发现大量“积分未变动,无需更新”的日志,但数据库却实实在在地在更新。这说明我判断积分是否变动的逻辑失效了。
(二) 问题定位:两个小时的煎熬
- 第一反应: 是不是缓存没清?是不是数据库旧数据?一通操作,问题依旧。
- 第二反应: 本地Debug。我把线上的数据在本地模拟了一遍,神奇的是,本地怎么测都是正常的!这让我几乎崩溃。
- 灵光一现: 我突然想到,本地测试的积分值很小,比如从10变成20。而线上出问题的用户,积分都很高,比如从10000变成10000。难道是……Integer缓存池?
我立刻在本地写了一个测试单元:
@Test
public void testIntegerCache() {
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // 输出 true,在缓存池内,是同一个对象
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // 输出 false,超出缓存池,是新对象
Integer e = 1000;
Integer f = 1000;
System.out.println(e.equals(f)); // 输出 true,永远用这个!
}破案了!我的代码里,正是用了if (currentScore == newScore)来做判断。
(三) 深度剖析:不只是Integer
==的本质: 它就是比较栈中的值。对于基本类型,栈里存的就是数值本身。对于引用类型,栈里存的是对象在堆中的内存地址。所以,==比较引用类型,就是看它们是不是同一个东西。equals的契约: 它的默认实现就是==,但Java的核心类库重写了它。String比较字符串序列,Integer比较包装的int值。- 缓存池(Interning): 不仅是
Integer(-128\~127),Long、Short、Byte、Character(0\~127)也有缓存。String也有字符串常量池。这是为了节省内存和提高性能。
(四) 举一反三:String的坑更常见
String s1 = "hello"; // 在字符串常量池
String s2 = "hello";
String s3 = new String("hello"); // 在堆里强制new一个新对象
String s4 = s3.intern(); // 手动放入常量池
System.out.println(s1 == s2); // true,常量池同一个对象
System.out.println(s1 == s3); // false,地址不同
System.out.println(s1 == s4); // true,s4是常量池里的那个对象
// 永远使用equals!
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true(五) 最佳实践和Code Review清单
现在,我和同事互相Review代码时,会特别注意:
- [ ] 所有包装类的比较,是否使用
.equals? - [ ] 所有字符串的比较,是否使用
.equals,并且是否采用"常量".equals(变量)的写法来避免空指针? - [ ] 如果有使用
enum,比较是否使用==?(因为enum值是单例,可以用==)
总结: 这个教训价值千金。它让我明白,对一门语言的基础理解深度,直接决定了你代码的健壮性。不要满足于“它能跑”,要追求“它为什么能跑”以及“它会在什么情况下跑崩”。