你好呀,我是歪歪。

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

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

我说我也不理解,把一个 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 int test() {
    return 3;
}

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

左边是不加 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 class Test {

    public static void main(String[] args) {
        ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
        queue.offer(new 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 的问题,但是也没有找到像是这一小节里面的问题的权威人士的认证。

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

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

我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!的更多相关文章

  1. 总是有一个程序的bug没找到

     算法训练 Lift and Throw   时间限制:3.0s   内存限制:256.0MB      问题描述 给定一条标有整点(1, 2, 3, ...)的射线. 定义两个点之间的距离为其下标之 ...

  2. 给IDEA道个歉,这不是它的BUG,而是反编译插件的BUG。

    你好呀,我是歪歪. 上周我不是发了<我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!>这篇文章吗. 主要描述了在 IDEA 里面反编译后的 class 文件中有这样的代码片段: 很明 ...

  3. WordPress访问打开速度很慢的几种解决方法

    最近WordPress网站访问特别的慢,有时间要加载一分钟才能完全打开,最初怀疑是服务器的问题,经过多方测试,还是没找到原因.后来,通过工具测出浏览器一直在加载fonts.googleapis.com ...

  4. React Native搭建开发环境 之 --走过的坑

    React Native是使用JavaScript和React编写原生移动应用 我的开发平台是基于windows系统,所以只支持android,要是想开发ios系统,那就只能考虑使用沙盒环境 接下来就 ...

  5. vue+vux scrollTop无法实现定位到具体dom

    先看一下最终的运行效果. 项目背景介绍:技术栈: vue+vux+nodejs需求:对汽车品牌列表可以按照字母进行索引定位 在开发中实现这种需求,心想还不是小菜一碟,作为一个饱经bug的程序员,别的我 ...

  6. CMake Error at cuda_compile_generated_warp.cu.o.cmake:264 (message)

    今天,我来给大家分享一下opencv安装时报的错.然后讲错是怎么解决的. 为啥老是写一些环境搭建的博客?因为环境搭建琐碎而繁杂,希望写下来,帮助大家.让大家少走弯路. 专注主业,专注算法的实现和优化. ...

  7. web项目no such method exception

    昨天更新包后出现这个异常,经过仔细全面排查,项目源码是没问题的. 怀疑jvm被重写了,肉眼也没找到证据.怀疑是操作系统问题,这个也不会没办法排查 于是给客户重新发了个war包,客户运行后出现 异常: ...

  8. 自学安卓练习作品单词APP(1)-安卓的hello word与有道字典防爬虫破解

    1.前言 闲来无聊.手机每天又是都接触的东西.程序什么的最容易接触到.想到有些人说前后端都做就是全栈的说法.哦,你看html5全栈. 要我说多接触一些多有意思.天天写后端.还不是业务层.又不是什么高大 ...

  9. centos7服务器搭建javaweb运行环境及代码部署

    之前在一直在学习java web终于写完了第一个小demo,于是在阿里云上买了一个服务器,开始了配置服务器环境的踩坑之旅.... ps:本文不讨论服务器配置的具体步骤,网上都很多,按部就班就是,本文主 ...

随机推荐

  1. spi详解

    来源:https://www.sohu.com/a/211324861_468626 1. SPI简介 SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围 ...

  2. 12 Web Development Trends That Will Dominate 2022

    12 Web Development Trends That Will Dominate 2022 (mindinventory.com) Progressive Web Apps (PWAs) An ...

  3. 论文解读(XR-Transformer)Fast Multi-Resolution Transformer Fine-tuning for Extreme Multi-label Text Classification

    Paper Information Title:Fast Multi-Resolution Transformer Fine-tuning for Extreme Multi-label Text C ...

  4. 洛谷 P5706 【深基2.例8】再分肥宅水

    题目连接: P5706 [深基2.例8]再分肥宅水 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 我提交的: 1 #include<iostream> 2 #inclu ...

  5. x64 番外篇——保护模式相关

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  6. 前端复制粘贴文字clipBoard.js的使用

    1. vue  中的复制粘贴: <div class="mainTextItem" @click="copyTXTOne" id="copyOn ...

  7. MySQL中MyISAM和InnoDB引擎的区别

    区别: 1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事 ...

  8. audio小记

    写H5活动页的需要音频,图标旋转停止保持当时的旋转角度,这样视觉体验效果好: 之前写法是点击pause()就直接停止动画,后来发现了animation有个比较好的属性animation-play-st ...

  9. 使用mockjs模拟后端返回的json数据;

    前后端分离开发中最重要的一部就是前后端联调,很多时候后端进度是跟不上前端的,所以需要前端模拟一些数据进行调试,这样前端的进度就可以加快了.后端的小哥哥别打我: 使用mockjs可以很方便的模拟出想要的 ...

  10. 自己写一个简单的LinkedList

    单链表 推荐阅读:https://www.cnblogs.com/zwtblog/tag/源码/ 哨兵节点: 哨兵节点在树和链表中被广泛用作伪头.伪尾等,通常不保存任何数据. 我们将使用伪头来简化我们 ...