共计 6361 个字符,预计需要花费 16 分钟才能阅读完成。
你好呀,我是歪歪。
前几天有敌人给我发来这样的一个截图:
他说他不了解,为什么这样不报错。
我说我也不了解,把一个 boolean 类型赋值给 int 类型,怎么会不报错呢,并接着诘问他:这个代码截图是哪里来的?
他说是 Lombok 的 @Data 注解主动生成的。
巧了,对于 Lombok 我之前有一点点理解,所以听到这个的答案的那一瞬间,电光火石之间我好像明确了点什么货色:因为 Lombok 是利用字节码加强的技术,间接操作字节码文件的,难道它能够间接绕过变量类型不匹配的问题?
然而很快又转念一想,不可能啊:这玩意要是都能绕过,Java 还玩个毛线啊。
于是我决定钻研一下,最初发现这事儿其实很简略:就是 idea 的一个 bug。
复现
Lombok 插件我原本也再用,所以我很快就在本地复现了一波。
源文件是这样的,我只是加了 @Data 注解:
通过 Maven install 编译之后的 class 文件是这样的:
能够看到 @Data 注解帮咱们干了十分多的事件:生成了无参构造函数、name 字段的 get/set 办法、equals 办法、toStrong 办法还有 hashCode 办法。
其实你点到 @Data 注解的源码外面去,它也给你阐明了,这就是一个复合注解:
因而,真正生成 hashCode 办法的注解,应该是 @EqualsAndHashCode 才对。
所以,为了排除烦扰项,不便我聚焦到 hashCode 办法上,我把 @Data 注解替换为 @EqualsAndHashCode:
后果还是一样的,只是默认生成的办法少了很多,而且我也不关怀那些办法。
当初,也眼见为实了,为啥这里的 hashCode 办法外面的第一行代码是这样的呢:
int PRIME = true;
直觉通知我,这里必定有障眼法。
我首先想到了另一个反编译的工具,jd-gui,就它:
果然,把 class 文件拖到 jd-gui 外面之后,hashCode 办法是这样的:
是数字 59,而不是 true 了。
然而这个 PRIME 变量,看起来在 hashCode 办法外面也没有用呢,这个问题不焦急,先抛出来放在这里,等下再说。
另外,我还想到了间接查看字节码的办法:
能够看到这样看到的 hashCode 办法的第一个命令用的整型入栈指令 bipush 数字 59。
通过 jd-gui 和字节码的验证,我有理由狐疑在 idea 外面显示 int PRIME = true
绝!对!是!BUG!
开心,又发现 BUG 了,素材这不就来了吗。
过后我开心极了,就和上面这个小朋友的表情是一样一样的。
线索
于是我在网上找了一圈,没有找到任何这方面的材料,没有一点点播种。心田的 OS 是:“啊,肯定是我的姿态不对,再来一次。”
扩充了搜寻范畴,又找了一圈。
“怎么还是没有什么线索呢,没道理啊!不行,肯定是有蛛丝马迹的。”
于是又又找一圈。
“嗯,的确是没有什么线索。节约我几小时,垃圾,就这样吧。”
我穷尽我的毕生所学,在网上翻了个底朝天,的确没有找到对于 idea 为什么会在这里显示 int PRIME = true
这样的一行代码。
我找到的惟一有相关度的问题是这个:
https://stackoverflow.com/que…
在这个问题外面,发问的哥们说,为什么他看到了 int result = true
这样的代码,且没有编译谬误?
和我看到的有点类似,然而又不是齐全一样。我发现他的 Test 类是无参的,而我本人的做测试的 UserInfo 是有一个 name 参数的。
于是我也搞了个无参的看了一下:
我这里是没有问题的,显示的是 int result = 1
。
而后有人问是不是因为你这个 Test 类没有字段呀,搞几个字段看看。
当他加了两个字段之后,编译后的 class 文件就和我看到的是一样的了:
然而这个问题上面只有这一个无效答复:
这个答复的哥们说:你看到 hashCode 办法是这样的,可能是因为你用的生成字节码的工具的一个问题。
在你用的工具的外部,布尔值 true 和 false 别离用 1 和 0 示意。
一些字节码反编译器自觉地将 0 翻译成 false,将 1 翻译成 true,这可能就是你遇到的状况。
这个哥们想表白的意思也是:这是工具的 BUG。
尽管我总是感觉差点意思,先不说差在哪儿了吧,按下不表,咱们先接着看。
在这个答复外面,还提到了 lombok 的一个个性 delombok,我想先说说这个:
delombok
这是个啥货色呢?
给你说个场景,假如你喜爱用 Lombok 的注解,于是你在你对外提供的 api 包外面应用了相干的注解。
然而援用你 api 包的同学,他并不喜爱 Lombok 注解,也没有做过相干依赖和配置,那你提供过来 api 包他人必定用不了。
那么怎么办呢?
delombok 就派上用场了。
能够间接生成曾经解析过 lombok 注解的 java 源代码。
官网上对于这块的形容是这样的:
https://projectlombok.org/fea…
换句话说,也就是你能够利用它看到 lombok 给你生成的 java 文件是长什么样的。
我带你瞅一眼是啥样的。
从官网上的形容能够看到 delombok 有很多不同的打开方式:
对咱们而言,最简略的计划就是间接用 maven plugin 了。
https://github.com/awhitford/…
间接把这一坨配置贴到我的项目的 pom.xml 外面就行了。
然而须要留神的是,这个配置上面还有一段话,结尾第一句就很重要:
Place the java source code with lombok annotations in src/main/lombok (instead of src/main/java).
将带有 lombok 注解的 java 源代码放在 src/main/lombok 门路下,而不是 src/main/java 外面。
所以,我创立了一个 lombok 文件夹,并且把这 UserInfo.java 文件挪动到了外面。
而后执行 maven 的 install 操作,能够看到 target/generated-sources/delombok 门路下多了一个 UserInfo.java 文件:
这个文件就是通过 delombok 插件解决之后的 java 文件,能够在遇到对方没有应用 lombok 插件的状况下,间接放到 api 外面提供进来。
而后咱们瞅一眼这个文件。我拿到这个文件次要还是想看看它 hashCode 办法到底是怎么样的:
看到没有,hashCode 办法外面的 int PRIME = true
没有了,取而代之的是 final int PRIME = 59
。
这曾经是 java 文件了,要是这中央还是 true 的话,那么妥妥的编译谬误:
而且通过 delombok 生成的源码,也解答了我之前的一个疑难:
看 class 文件的时候,感觉 PRIME 这个变量没有应用过呢,那么它的意义是什么呢?
然而看 delombok 编译后失去的 java 文件,我晓得了,PRIME 其实是用到了的:
那么为啥 PRIME 变成了 true 呢?
望着 delombok 生成的源码,我忽然眼前一亮,好家伙,你看这是什么:
这是 final 类型的局部变量。
留神:是!final!类!型!
为了更好的引出上面我想说的概率,我先给你写一个非常简单的货色:
看到了吗,why 和 mx 都变成 true 了,相当于把 test 办法间接批改为这样了:
public int test() {return 3;}
给你看看字节码可能更加直观一点:
右边是不加 final,左边是加了 final。
能够看到,加了 final 之后齐全都没有拜访局部变量的 iload 操作了。
这货色叫什么?
这就是“常量折叠”。
有幸很久之前看到过 JVM 大佬 R 大对于这个景象的解读,过后感觉很乏味,所以有点印象。
当看到 final int PRIME = 59
的时候,一下就点燃了回顾。
于是去找到了之前看的链接:
https://www.zhihu.com/questio…
在 R 大的答复中,有这么一小段,我给你截图看看:
同时,给你看看 constant variable 这个货色在 Java 语言标准外面的定义:
A variable of primitive type or type String, that is final and initialized with a compile-time constant expression , is called a constant variable.
一个根本类型或 String 类型的变量,如果是被 final 润饰的,在编译时的就实现了初始化,这就被称为 constant variable(常量变量)。
所以 final int PRIME = 59
外面的 PRIME 就是一个常量变量。
这里既然提到了 String,那我也给你举个例子:
你看 test2 办法,用了 final,最终的 class 文件中,间接就是 return 了拼接实现后的字符串。
为什么呢?
别问,问就是规定。
https://docs.oracle.com/javas…
我只是在这里给你指个路,有趣味的能够本人去翻一翻。
另外,也再一次实锤了 class 文件上面这样的显示,的确是 idea 的 BUG,和 lombok 齐全没有任何关系,因为我这里基本就没有用 lombok:
同时,对于下面这个问题在 lombok 的 github 外面也有相干的探讨:
https://github.com/projectlom…
提问者说:这个 PRIME 变量看起来像是没啥用的代码呢,因为在这个部分办法中都没有被应用过。
官网的答复是:老哥,我狐疑你看到的是 javac 的一个优化。如果你看一下 delombok 生成的代码,你会看到 PRIME 这个玩意是在被应用。应该是 javac 在对这个常量进行了内联的操作。
为什么是 59
咱们再次把眼光聚焦到 delombok 生成的 hashCode 办法:
为什么这里用了 59 呢,hashCode 外面的因子不应该是无脑应用 31 吗?
我感觉这里是有故事的,于是我又浅挖了一下。
我挖线索的思路是这样的。
首先我先找到 59 这个数是怎么来的,它必定是来自于 lombok 的某个文件中。
而后我把 lombok 的源码拉下来,查看对应文件中针对这个值的提交或者说变动。失常状况下,这种魔法值不会是平白无故的来的,提交代码的时候大概率会针对为什么取这个值进行一个阐明。
我只有找到那段阐明即可。
首先,我依据 @EqualsAndHashCode 调用的中央,找到了这个类:
lombok.javac.handlers.HandleEqualsAndHashCode
而后在这个类外面,能够看到咱们相熟的“PRIME”:
接着,搜寻这个关键词,我找到了这个中央:
这里的这个办法,就是 59 的起源:
lombok.core.handlers.HandlerUtil#primeForHashcode
第一步就算实现了,接着就要去看看 lombok 外面 HandlerUtil 这个类的提交记录了:
后果很顺利,这个类的第二次提交的 commit 信息就在说为什么没有用 31。
从 commit 信息看,之前应该用的就是 31,而用这个数的起因是因为《Effective Java》举荐应用。然而依据 issue#625 外面的观点来说,兴许 277 是一个比拟好的值。
从提交的代码也能够看出,之前的确是应用的 31,而且是间接写死的:
在这次的提交外面,批改为了 277 并提到了 HandlerUtil 的一个常量中:
然而,这样不是我想要找的 59 呀,于是接着找。
很快,就找到了 277 到 59 的这一次变更:
同时也指向了 issue#625。
等我哼着小曲唱着歌,筹备到 issue#625 外面一探到底的时候,傻眼了:
https://github.com/projectlom…
issue#625 说的事儿基本和 hashCode 没有任何关系呀。
而且这个问题是 2015 年 7 月 15 日才提出来的,然而代码可是在 2014 年 1 月就提交了。
所以 lombok 的 issues 必定是失落了很大一部分,导致当初我对不上号了。
这行为,属于在代码外面下毒了,我就是一个中毒的人。
事件看起来就像是走进了死胡同。
然而很快,就峰回路转了,因为我的小脑壳外面闪过了另外一个可能有答案的中央,那就是 changelog:
https://projectlombok.org/cha…
果然,在 changelog 外面,我发现了新的线索 issue#660:
关上 issue#660 一看,嗯,这次应该是没走错路了:
https://github.com/projectlom…
在这个 issues 外面首先 Maaartinus 老哥给出了一段代码,而后他解释说:
在我的例子中,如果 lombok 生成的 hashCode 办法应用 31 这个因子,对于 256 个生成的对象,只有 64 个惟一的哈希值,也就是说会产生十分多的碰撞。
然而如果 lombok 应用一个更好的因子,这个数字会减少到 144,绝对好一点。
而且简直任何奇数都能够。应用 31 是多数蹩脚的抉择之一。
官网看到后,很快就给了回复:
看了老哥的程序,我感觉老哥说的有情理啊。之前我用 31 也齐全是因为《Effective Java》外面是这样倡议的,没有思考太多。
另外,我决定应用 277 这个数字来代替 31,作为新的因子。
为什么是 277 呢?
别问,问就是它很 lucky!
277 is the lucky winner
那么最初为什么又从 277 批改为 59 呢?
因为应用 227 这样一个“微小”的因子,会有大略 1-2% 的性能损失。所以须要换一个数字。
最终决定就选 59 了,尽管也没有说具体起因:
然而联合 changelog 来看,我有理由猜想起因之一是要选一个小于 127 的数,因为 -128 到 127 在 Integer 的缓存范畴内:
IDEA
说起 IDEA 的 BUG,我早年间可是踩过一次印象粗浅的“BUG”。
以前在调试 ConcurrentLinkedQueue 这个货色的,间接把心态给玩崩了。
你有可能会碰到的一个巨坑,比方咱们的测试代码是这样的:
public class Test {public static void main(String[] args) {ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
queue.offer(new Object());
}
}
非常简单,在队列外面增加一个元素。
因为初始化的状况下 head=tail=new Node<E>(null):
所以在 add 办法被调用之后的链表构造外面的 item 指向应该是这样的:
咱们在 offer 办法外面退出几个输入语句:
执行之后的日志是这样的:
为什么最初一行输入,【offer 之后】输入的日志不是 null->@723279cf 呢?
因为这个办法外面会调用 first 办法,获取真正的头节点,即 item 不为 null 的节点:
到这里都一切正常。然而,当你用 debug 模式操作的时候就不太一样了:
头节点的 item 不为 null 了!而头节点的下一个节点为 null,所以抛出空指针异样。
单线程的状况下代码间接运行的后果和 Debug 运行的后果不统一 !这不是遇到鬼了吗。
我在网上查了一圈,发现遇到鬼的网友还不少。
最终找到了这个中央:
https://stackoverflow.com/que…
这个哥们遇到的问题和咱们截然不同:
这个问题上面只有一个答复:
你晓得答复这个问题的哥们是谁吗?
IDEA 的产品经理,献上我的 respect。
最初的解决方案就是敞开 IDEA 的这两个配置:
因为 IDEA 在 Debug 模式下会被动的帮咱们调用一次 toString 办法,而 toString 办法外面,会去调用迭代器。
而 CLQ 的迭代器,会触发 first 办法,这个外面和之前说的,会批改 head 元素:
所有,都水落石出了。
而这篇文章外面的问题:
我有理由确定就是 IDEA 的问题,然而也没有找到像是这一大节外面的问题的权威人士的认证。
所以我后面说的差点意思,就是这个意思。
— 本文首发于公众号 why 技术。