4

我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!

 1 year ago
source link: https://www.cnblogs.com/thisiswhy/p/16276549.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!

你好呀,我是歪歪。

前几天有朋友给我发来这样的一个截图:

20220514162420.png

他说他不理解,为什么这样不报错。

我说我也不理解,把一个 boolean 类型赋值给 int 类型,怎么会不报错呢,并接着追问他:这个代码截图是哪里来的?

他说是 Lombok 的 @Data 注解自动生成的。

巧了,对于 Lombok 我之前有一点点了解,所以听到这个的答案的那一瞬间,电光火石之间我仿佛明白了点什么东西:因为 Lombok 是利用字节码增强的技术,直接操作字节码文件的,难道它可以直接绕过变量类型不匹配的问题?

但是很快又转念一想,不可能啊:这玩意要是都能绕过,Java 还玩个毛线啊。

于是我决定研究一下,最后发现这事儿其实很简单:就是 idea 的一个 bug。

20220515133749.png

Lombok 插件我本来也再用,所以我很快就在本地复现了一波。

源文件是这样的,我只是加了 @Data 注解:

20220514164106.png

经过 Maven install 编译之后的 class 文件是这样的:

20220514164328.png

可以看到 @Data 注解帮我们干了非常多的事情:生成了无参构造函数、name 字段的 get/set 方法、 equals 方法、toStrong 方法还有 hashCode 方法。

其实你点到 @Data 注解的源码里面去,它也给你说明了,这就是一个复合注解:

20220514170921.png

因此,真正生成 hashCode 方法的注解,应该是 @EqualsAndHashCode 才对。

所以,为了排除干扰项,方便我聚焦到 hashCode 方法上,我把 @Data 注解替换为 @EqualsAndHashCode:

20220514171503.png

结果还是一样的,只是默认生成的方法少了很多,而且我也不关心那些方法。

现在,也眼见为实了,为啥这里的 hashCode 方法里面的第一行代码是这样的呢:

int PRIME = true;

直觉告诉我,这里肯定有障眼法。

我首先想到了另一个反编译的工具,jd-gui,就它:

20220514171940.png

果然,把 class 文件拖到 jd-gui 里面之后,hashCode 方法是这样的:

20220514172342.png

是数字 59,而不是 true 了。

但是这个 PRIME 变量,看起来在 hashCode 方法里面也没有用呢,这个问题不着急,先抛出来放在这里,等下再说。

另外,我还想到了直接查看字节码的方法:

20220515134024.png

可以看到这样看到的 hashCode 方法的第一个命令用的整型入栈指令 bipush 数字 59。

经过 jd-gui 和字节码的验证,我有理由怀疑在 idea 里面显示 int PRIME = true 绝!对!是!BUG!

开心,又发现 BUG 了,素材这不就来了吗。

20220515134800.png

当时我开心极了,就和下面这个小朋友的表情是一样一样的。

668b056fgy1h0r6lj1eakg209f09lkjm.gif

于是我在网上找了一圈,没有找到任何这方面的资料,没有一点点收获。内心的 OS 是:“啊,一定是我的姿势不对,再来一次。”

扩大了搜索范围,又找了一圈。

“怎么还是没有什么线索呢,没道理啊!不行,一定是有蛛丝马迹的。”

于是又又找一圈。

“嗯,确实是没有什么线索。浪费我几小时,垃圾,就这样吧。”

20220514175307.png

我穷尽我的毕生所学,在网上翻了个底朝天,确实没有找到关于 idea 为什么会在这里显示 int PRIME = true 这样的一行代码。

我找到的唯一有相关度的问题是这个:

https://stackoverflow.com/questions/70824467/lombok-hashcode-1-why-good-2-why-no-compile-error/70824612#70824612

20220514180852.png

在这个问题里面,提问的哥们说,为什么他看到了 int result = true 这样的代码,且没有编译错误?

和我看到的有点相似,但是又不是完全一样。我发现他的 Test 类是无参的,而我自己的做测试的 UserInfo 是有一个 name 参数的。

于是我也搞了个无参的看了一下:

20220514181612.png

我这里是没有问题的,显示的是 int result = 1

然后有人问是不是因为你这个 Test 类没有字段呀,搞几个字段看看。

当他加了两个字段之后,编译后的 class 文件就和我看到的是一样的了:

20220514181926.png

但是这个问题下面只有这一个有效回答:

20220514182031.png

这个回答的哥们说:你看到 hashCode 方法是这样的,可能是因为你用的生成字节码的工具的一个问题。

在你用的工具的内部,布尔值 true 和 false 分别用 1 和 0 表示。

一些字节码反编译器盲目地将 0 翻译成 false,将 1 翻译成 true,这可能就是你遇到的情况。

这个哥们想表达的意思也是:这是工具的 BUG。

虽然我总是觉得差点意思,先不说差在哪儿了吧,按下不表,我们先接着看。

在这个回答里面,还提到了 lombok 的一个特性 delombok,我想先说说这个:

20220514184125.png

delombok

这是个啥东西呢?

给你说个场景,假设你喜欢用 Lombok 的注解,于是你在你对外提供的 api 包里面使用了相关的注解。

但是引用你 api 包的同学,他并不喜欢 Lombok 注解,也没有做过相关依赖和配置,那你提供过去 api 包别人肯定用不了。

那么怎么办呢?

delombok 就派上用场了。

可以直接生成已经解析过 lombok 注解的 java 源代码。

官网上关于这块的描述是这样的:

https://projectlombok.org/features/delombok

20220514192003.png

换句话说,也就是你可以利用它看到 lombok 给你生成的 java 文件是长什么样的。

我带你瞅一眼是啥样的。

从官网上的描述可以看到 delombok 有很多不同的打开方式:

20220514192350.png

对我们而言,最简单的方案就是直接用 maven plugin 了。

https://github.com/awhitford/lombok.maven

20220514193746.png

直接把这一坨配置贴到项目的 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 文件:

20220514194230.png

这个文件就是经过 delombok 插件处理之后的 java 文件,可以在遇到对方没有使用 lombok 插件的情况下,直接放到 api 里面提供出去。

然后我们瞅一眼这个文件。我拿到这个文件主要还是想看看它 hashCode 方法到底是怎么样的:

20220514194616.png

看到没有,hashCode 方法里面的 int PRIME = true 没有了,取而代之的是 final int PRIME = 59

20220515134529.png

这已经是 java 文件了,要是这地方还是 true 的话,那么妥妥的编译错误:

20220514194800.png

而且通过 delombok 生成的源码,也解答了我之前的一个疑问:

看 class 文件的时候,感觉 PRIME 这个变量没有使用过呢,那么它的意义是什么呢?

但是看 delombok 编译后得到的 java 文件,我知道了,PRIME 其实是用到了的:

20220514212255.png

那么为啥 PRIME 变成了 true 呢?

望着 delombok 生成的源码,我突然眼前一亮,好家伙,你看这是什么:

20220514234230.png

这是 final 类型的局部变量。

注意:是!final!类!型!

为了更好的引出下面我想说的概率,我先给你写一个非常简单的东西:

20220514235637.png

看到了吗,why 和 mx 都变成 true 了,相当于把 test 方法直接修改为这样了:

public int test() {
    return 3;
}

给你看看字节码可能更加直观一点:

20220515000351.png

左边是不加 final,右边是加了 final。

可以看到,加了 final 之后完全都没有访问局部变量的 iload 操作了。

这东西叫什么?

这就是“常量折叠”。

有幸很久之前看到过 JVM 大佬R大对于这个现象的解读,当时觉得很有趣,所以有点印象。

当看到 final int PRIME = 59 的时候,一下就点燃了回忆。

于是去找到了之前看的链接:

https://www.zhihu.com/question/21762917/answer/19239387

20220515001309.png

在R大的回答中,有这么一小段,我给你截图看看:

20220515001417.png

同时,给你看看 constant variable 这个东西在 Java 语言规范里面的定义:

20220515130558.png

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,那我也给你举个例子:

20220515131758.png

你看 test2 方法,用了 final,最终的 class 文件中,直接就是 return 了拼接完成后的字符串。

为什么呢?

别问,问就是规定。

https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.28

20220515132010.png

我只是在这里给你指个路,有兴趣的可以自己去翻一翻。

另外,也再一次实锤了 class 文件下面这样的显示,确实是 idea 的 BUG,和 lombok 完全没有任何关系,因为我这里根本就没有用 lombok:

20220515001627.png

同时,关于上面这个问题在 lombok 的 github 里面也有相关的讨论:

https://github.com/projectlombok/lombok/issues/523

20220515002718.png

提问者说:这个 PRIME 变量看起来像是没啥用的代码呢,因为在这个局部方法中都没有被使用过。

官方的回答是:老哥,我怀疑你看到的是 javac 的一个优化。如果你看一下 delombok 生成的代码,你会看到 PRIME 这个玩意是在被使用。应该是 javac 在对这个常量进行了内联的操作。

为什么是 59

我们再次把目光聚焦到 delombok 生成的 hashCode 方法:

20220515002053.png

为什么这里用了 59 呢,hashCode 里面的因子不应该是无脑使用 31 吗?

我觉得这里是有故事的,于是我又浅挖了一下。

我挖线索的思路是这样的。

首先我先找到 59 这个数是怎么来的,它肯定是来自于 lombok 的某个文件中。

然后我把 lombok 的源码拉下来,查看对应文件中针对这个值的提交或者说变化。正常情况下,这种魔法值不会是无缘无故的来的,提交代码的时候大概率会针对为什么取这个值进行一个说明。

我只要找到那段说明即可。

首先,我根据 @EqualsAndHashCode 调用的地方,找到了这个类:

lombok.javac.handlers.HandleEqualsAndHashCode

20220515004154.png

然后在这个类里面,可以看到我们熟悉的 “PRIME”:

20220515004219.png

接着,搜索这个关键词,我找到了这个地方:

20220515004410.png

这里的这个方法,就是 59 的来源:

lombok.core.handlers.HandlerUtil#primeForHashcode

20220515004526.png

第一步就算完成了,接着就要去看看 lombok 里面 HandlerUtil 这个类的提交记录了:

20220515005059.png

结果很顺利,这个类的第二次提交的 commit 信息就在说为什么没有用 31。

从 commit 信息看,之前应该用的就是 31,而用这个数的原因是因为《Effective Java》推荐使用。但是根据 issue#625 里面的观点来说,也许 277 是一个比较好的值。

从提交的代码也可以看出,之前确实是使用的 31,而且是直接写死的:

20220515005412.png

在这次的提交里面,修改为了 277 并提到了 HandlerUtil 的一个常量中:

20220515005531.png

但是,这样不是我想要找的 59 呀,于是接着找。

很快,就找到了 277 到 59 的这一次变更:

20220515005635.png

同时也指向了 issue#625。

等我哼着小曲唱着歌,准备到 issue#625 里面一探究竟的时候,傻眼了:

https://github.com/projectlombok/lombok/issues/625

20220515005855.png

issue#625 说的事儿根本和 hashCode 没有任何关系呀。

而且这个问题是 2015 年 7 月 15 日才提出来的,但是代码可是在 2014 年 1 月就提交了。

所以 lombok 的 issues 肯定是丢失了很大一部分,导致现在我对不上号了。

这行为,属于在代码里面下毒了,我就是一个中毒的人。

20220515134730.png

事情看起来就像是走进了死胡同。

但是很快,就峰回路转了,因为我的小脑壳里面闪过了另外一个可能有答案的地方,那就是 changelog:

https://projectlombok.org/changelog

果然,在 changelog 里面,我发现了新的线索 issue#660:

20220515010429.png

打开 issue#660 一看,嗯,这次应该是没走错路了:

https://github.com/projectlombok/lombok/issues/660

在这个 issues 里面首先 Maaartinus 老哥给出了一段代码,然后他解释说:

20220515011537.png

在我的例子中,如果 lombok 生成的 hashCode 方法使用 31 这个因子,对于 256 个生成的对象,只有 64 个唯一的哈希值,也就是说会产生非常多的碰撞。

但是如果 lombok 使用一个更好的因子,这个数字会增加到 144,相对好一点。

而且几乎任何奇数都可以。使用 31 是少数糟糕的选择之一。

官方看到后,很快就给了回复:

20220515012050.png

看了老哥的程序,我觉得老哥说的有道理啊。之前我用 31 也完全是因为《Effective Java》里面是这样建议的,没有考虑太多。

另外,我决定使用 277 这个数字来替代 31,作为新的因子。

为什么是 277 呢?

别问,问就是它很 lucky!

277 is the lucky winner

那么最后为什么又从 277 修改为 59 呢?

20220515012431.png

因为使用 227 这样一个“巨大 ”的因子,会有大概 1-2% 的性能损失。所以需要换一个数字。

最终决定就选 59 了,虽然也没有说具体原因:

20220515012611.png

但是结合 changelog 来看,我有理由猜测原因之一是要选一个小于 127 的数,因为 -128 到 127 在 Integer 的缓存范围内:

20220515012804.png

说起 IDEA 的 BUG,我早年间可是踩过一次印象深刻的 “BUG”。

以前在调试 ConcurrentLinkedQueue 这个东西的,直接把心态给玩崩了。

20220515132513.png

你有可能会碰到的一个巨坑,比如我们的测试代码是这样的:

public class Test {

    public static void main(String[] args) {
        ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
        queue.offer(new Object());
    }
}

非常简单,在队列里面添加一个元素。

由于初始化的情况下 head=tail=new Node(null):

20220515132832.png

所以在 add 方法被调用之后的链表结构里面的 item 指向应该是这样的:

20220515132858.png

我们在 offer 方法里面加入几个输出语句:

20220515132914.png

执行之后的日志是这样的:

20220515132936.png

为什么最后一行输出,【offer之后】输出的日志不是 null->@723279cf 呢?

因为这个方法里面会调用 first 方法,获取真正的头节点,即 item 不为 null 的节点:

20220515132956.png

到这里都一切正常。但是,当你用 debug 模式操作的时候就不太一样了:

20220515133018.png

头节点的 item 不为 null 了!而头节点的下一个节点为 null,所以抛出空指针异常。

单线程的情况下代码直接运行的结果和 Debug 运行的结果不一致!这不是遇到鬼了吗。

20220515133041.png

我在网上查了一圈,发现遇到鬼的网友还不少。

最终找到了这个地方:

https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly

这个哥们遇到的问题和我们一模一样:

20220515133121.png

这个问题下面只有一个回答:

20220515133146.png

你知道回答这个问题的哥们是谁吗?

20220515133203.png

IDEA 的产品经理,献上我的 respect。

20220515133232.png

最后的解决方案就是关闭 IDEA 的这两个配置:

20220515133258.png

因为 IDEA 在 Debug 模式下会主动的帮我们调用一次 toString 方法,而 toString 方法里面,会去调用迭代器。

而 CLQ 的迭代器,会触发 first 方法,这个里面和之前说的,会修改 head 元素:

20220515133317.png

一切,都真相大白了。

而这篇文章里面的问题:

20220514162420.png

我有理由确定就是 IDEA 的问题,但是也没有找到像是这一小节里面的问题的权威人士的认证。

所以我前面说的差点意思,就是这个意思。

--- 本文首发于公众号why技术。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK