那行“没问题”的代码,差点让我在凌晨四点怀疑JDK

🔑 关键词:Integer缓存,调试心理,认知偏见,代码审查,防御性编程

📖 摘要:一次Integer缓存引发的深夜排查,一段关于调试、认知偏见与“承认自己错了”的记录。

凌晨两点半,我盯着一行代码看了快两个小时。一杯水从热的放成了凉的,中间还跑去窗口发了会儿呆。那行代码写得很干净,干净到让我觉得它是无辜的:Map<String, Integer> map = someMap; int type = map.get("type"); if (type == 127) { ... }。我甚至把它抄在纸上,用不同的姿势看了一遍又一遍,横着、竖着、倒着,心里想它到底哪里有问题。最让我崩溃的是,我完全找不到问题,而这让我更坚信:一定是JDK的bug,一定是框架的bug,一定是运维今天偷偷改了什么东西。我可真聪明,从来不怀疑自己。

图片

后来我加了日志,重新丢到预发环境。等了大概几分钟,日志给我吐出来一个数字:127。type的值确实是127,但type == 127的结果是false。事到如今,再蠢也该反应过来了——Java的Integer缓存这档子事,-128到127之间的Integer对象是同一个,超过127就是两个不同的对象。用==比较两个Integer对象,其实是在比较“是不是同一个盒子”,而不是比较“里面的数字是不是一样”。这个知识点我考过试,但当时真正没意识到的是:我盯着代码看的那两个小时,我的大脑不是在找bug,而是在给错误找理由,一个接一个,越找越自信。它默认我是对的,然后把所有现实都往“我是对的”这个方向去解释。

图片

调试器是最容易骗人的工具。它把时间冻结住,给你一个干干净净的快照:此刻变量是什么值,栈走到了哪一行,一切都在掌控之中。但线上出问题的场景,往往不是那一刻的变量值有问题,而是时序、并发、状态在某一瞬间交错出来,调试器根本看不到那一瞬间。日志倒是连续的,但日志不会告诉你它漏写了什么。那次解决问题,最终靠的不是调试器不是日志,是排查很久之后重新翻开JDK文档,看到Integer这个类的缓存说明,才狠狠拍了一下自己的大腿。我管这叫“出洋相式编程”,它可能比任何高级技巧都更接近真相。

图片

还有一个更残酷的事实:把那段代码发给同事,可能三十秒他就会指出==这里比较的是包装类型。我为什么看不到?因为我不是在检查代码,我是在给自己找不在场证明。我会觉得,我之前既然这么写,肯定是有道理的。这个想法像一层很厚的纱窗,现实感只能透进来一点点。所以后来我修bug的时候,不允许自己只看眼前这个点。低级修法是把这个==换成equals;中级修法是返回类型改成int,所有包装类型比较的地方全部改成基本类型比较;高级修法是去问一句:为什么这个工具类会返回包装类型,为什么这里要用==,为什么这代码活着的时候没人觉得不对劲——让错误变成制度,写进规范里,防止它再生。

图片

还有一种情况,修完了你会后悔。有一回我用二分法定位到一个看起来很可疑的变量,把它替换掉之后,立刻冒出来七八个新的错误。为什么呢?因为那个变量是个“约定”,其他几处不受欢迎的代码都在靠它艰难地耦合着。我那时候才明白,一个bug不是一个孤立的错误,它是一个暴露点,是你对这段代码的历史、语境和别人的意图一无所知的暴露点。修复一个bug,有时候等于打开了旧账本。代码调试最深的功夫,不是用断点,是用敬畏心——承认在你之前或者在你旁边写代码的人,也不全是傻子;承认你看到“错误”的那一刹那,可能只是你还没看到它为什么正确。

图片

最后我把那个Integer的坑改了,测试全部通过,发完版本已经是凌晨四点半。没有特别兴奋,只觉得累,还带点后怕。我下楼想买个宵夜,便利店关了门,门口的野猫倒是睡得正香。我站在路灯下面,忽然想起一个问题:如果我一直相信自己是对的,我现在大概还在工位上跟那行代码死磕,而这个错误会在六点上线,烧掉整个早上。代码这个东西,教我最多的不是怎么写,而是怎么认错。我见过很多从不怀疑自己的人,他们写出来的东西看着板正,其实最容易翻车。

图片

🏷️ 标签: