关于java:我怀疑这是IDEA的BUG但是我翻遍全网没找到证据

29次阅读

共计 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 技术。

正文完
 0