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

你好呀,我是歪歪。

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

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

我说我也不理解,把一个 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/questions/70824467/lombok-hashcode-1-why-good-2-why-no-compile-error/70824612#70824612

在这个问题里面,提问的哥们说,为什么他看到了 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/features/delombok

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

我带你瞅一眼是啥样的。

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

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

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

直接把这一坨配置贴到项目的 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&#xA0;int&#xA0;<span class="hljs-function"><span class="hljs-title">test</span></span>()&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-built_in">return</span>&#xA0;3;<br>}

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

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

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

这东西叫什么?

这就是”常量折叠”。

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

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

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

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

在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/javase/specs/jls/se7/html/jls-15.html#jls-15.28

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

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

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

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

提问者说:这个 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/projectlombok/lombok/issues/625

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

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

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

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

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

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

https://projectlombok.org/changelog

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

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

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

在这个 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&#xA0;class&#xA0;Test&#xA0;{<br><br>&#xA0;&#xA0;&#xA0;&#xA0;public&#xA0;static&#xA0;void&#xA0;main(String[]&#xA0;args)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;ConcurrentLinkedQueue<object>&#xA0;queue&#xA0;=&#xA0;new&#xA0;ConcurrentLinkedQueue<>();<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;queue.offer(new&#xA0;Object());<br>&#xA0;&#xA0;&#xA0;&#xA0;}<br>}<br></object>

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

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

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

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

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

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

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

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

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

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

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

最终找到了这个地方:

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

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

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

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

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

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

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

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

一切,都真相大白了。

而这篇文章里面的问题:

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

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

Original: https://www.cnblogs.com/thisiswhy/p/16276549.html
Author: why技术
Title: 我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571099/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Java反射学习笔记:getParameterTypes和getGenericParameterTypes区别

    环境 Java:1.8Intellij IDEA:2019.2.4 前言 最近在写导出程序,对getGenericParameterTypes和getParameterTypes两…

    Java 2023年5月29日
    082
  • Java 8中Collectors.toMap空指针异常源码分析

    当需要将一个List转换为Map时,可以使用 Java 8 中的 Collectors.toMap() 方法,Map是由key-value组成的键值对集合,在使用 Collecto…

    Java 2023年6月8日
    082
  • JavaSE基础笔记(1)

    1、注释 // / / /* / 单行注释 多行注释 文档注释 2、标识符 3、数据类型 整数类型 byte占1个字节范围:-128~127 short占2个字节范围:-32768…

    Java 2023年6月13日
    070
  • SpringBoot接口-如何优雅的对参数进行校验?

    在以SpringBoot开发Restful接口时, 对于接口的查询参数后台也是要进行校验的,同时还需要给出校验的返回信息放到上文我们统一封装的结构中。那么如何优雅的进行参数的统一校…

    Java 2023年6月6日
    098
  • jq命令用法总结

    原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 如果说要给Linux文本三剑客(grep、sed、awk)添加一员的话,我觉得应该是jq命令,因为j…

    Java 2023年6月7日
    077
  • synchronized 是可重入锁吗?为什么?

    什么是可重入锁? 若一个程序或子程序可以”在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentr…

    Java 2023年6月14日
    076
  • 好物合集(2)

    Utools(超好用的插件软件) 是什么 uTools 是一个极简、插件化的现代桌面软件,通过自由选配丰富的插件,打造得心应手的工具集合。 通过快捷键(默认 alt + space…

    Java 2023年6月5日
    081
  • Cobol代码通过工具自动生成java代码 展示版

    本例是通过工具将cobol代码自动生成java代码。生成后的java代码是按照java编程风格生成的,完全屏蔽了cobol的特性。 一个cobol代码生成了4个java代码,分别说…

    Java 2023年6月8日
    074
  • Java是编译型语言还是解释型语言

    Java是编译型语言还是解释型语言 答案:java既是编译型语言,也是解释型语言。 你可以说它是编译型的。因为所有的Java代码都需要经过javac编译为.class文件,但主要是…

    Java 2023年6月13日
    053
  • Linux 进程管理

    Linux 进程管理 在 LINUX 中,每个执行的程序都称为一个进程。每一个进程都分配一个 ID 号(pid,进程号)。 每个进程都可能以两种方式存在的。前台与后台,所谓前台进程…

    Java 2023年6月5日
    077
  • Twikoo私有化部署教程–迁移腾讯云

    备份数据 私有化部署 创建容器 导入数据 重新配置twikoo面板设置 引入前端CDN Nginx https反代http 作者:小牛呼噜噜 | https://xiaoniuhu…

    Java 2023年6月15日
    089
  • S3上传时报错:Data read has a different length than the expected

    报错信息 使用S3上传文件时,发现存在几类报错。 第一种:Data read has a different length than the expected: dataLengt…

    Java 2023年6月5日
    058
  • java中final关键字

    介绍 final中文意思:最终的最后的 final 可以修饰类,属性,方法,局部变量,形参 使用场景 当不希望类被被继承时,可以用final修饰类 但不希望父类的某个方法,被子类重…

    Java 2023年6月6日
    093
  • 括号匹配

    括号匹配 题目 题目链接 代码 #include #include #include #include using namespace std; string str; stack…

    Java 2023年6月16日
    089
  • Java判定一个数值是否在指定的开闭区间范围内

    对于开闭区间,在数学中的表示方式通常为 () 和 [],小括号代表开放区间,中括号代表封闭区间,而它们的区别主要在于是否包含 = 等于号,开闭区间通常会分为以下一些情形: (1, …

    Java 2023年6月8日
    052
  • 26.服务端单线程模式下性能瓶颈测试

    VS2015 提供的性能探查器,可以看到程序的哪部分代码占用了多少的cpu 在Release版本下,使用,性能探查器———开始 运行一段时间之后…

    Java 2023年5月29日
    084
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球