大家好,我是伟伟。前几天有朋友给我发了这样一张截图:他说不明白为什么不报这样的错误。我说我也没看懂,把boolean类型赋给int类型怎么会不报错,然后问他:这个代码截图哪里来的?他说是Lombok的@Data注解自动生成的。无独有偶,之前对Lombok略有了解,所以听到答案的那一刻,我仿佛明白了什么:因为Lombok使用了字节码增强技术,直接操作字节码文件,是否可以直接绕过变量类型不匹配的问题?但是我很快转念一想,不可能:如果这个东西能绕过,Java还是在打羊毛。于是决定研究一下,最后发现这个东西其实很简单:就是idea中的一个bug。我也是用Lombok插件复现的,所以很快就在本地复现了。源文件是这样的,我只是加了@Data注解:Maveninstall编译的class文件是这样的:可以看到@Data注解帮我们做了很多事情:生成无参构造函数,name字段get/set方法、equals方法、toStrong方法和hashCode方法。其实你去看@Data注解的源码,它也给你解释了这是一个复合注解:因此,真正生成hashCode方法的注解应该是@EqualsAndHashCode。所以,为了排除杂念,让我更容易把注意力放在hashCode方法上,我把@Data注解换成了@EqualsAndHashCode:结果还是一样,只是默认生成的方法少了很多,我不不关心那些方法。现在,眼见为实,为什么这里的hashCode方法中的第一行代码是这样的:intPRIME=true;直觉告诉我,这里一定有猫腻。我首先想到另一个反编译工具jd-gui,就是这样:果然,将class文件拖入jd-gui后,hashCode方法是这样的:是数字59,不对。但是这个PRIME变量在hashCode方法中好像没什么用。不要担心这个问题。先丢在这里,以后再说。另外,我还想到了一个直接查看字节码的方法:可以看到这样看到的hashCode方法的第一条命令中用到的integerpush指令bipushnumber59。经过jd-gui和字节码的验证,我有理由怀疑idea中显示的是intPRIME=true。绝对地!正确的!是的!漏洞!真高兴,又发现bug了,素材不在这里吗?那时,我很开心,就像下面那个孩子的表情一样。线索于是在网上四处搜索,也没有找到这方面的资料,也没有得到一点点收获。内心OS是:“啊,一定是我的姿势不对,再试一次。”扩大搜索范围,再次四处搜索。“怎么还是没有线索?说不通!不对,一定是有线索。”于是我又搜索了一遍。“嗯,真的没什么头绪,浪费我几个小时,垃圾,就这样。”我用尽毕生的知识,在网上搜索了一下,实在是找不到idea为什么会显示intPRIME=heretrue这样一行代码。我找到的唯一相关的问题是这个:https://stackoverflow.com/que...这个问题问问题的哥们说为什么看到intresult=true这样的代码,而且没有编译错误?和我看到的差不多,但又不完全一样。我发现他的Test类没有参数,而我自己测试用的UserInfo有name参数。于是不加参数也看了看:我这边没问题,显示的是intresult=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插件。https://github.com/awhitford/...只需将这一块配置粘贴到项目的pom.xml中即可。但是需要注意的是这个配置下面有一段,开头的第一句话很重要:将带有lombok注解的java源代码放在src/main/lombok(而不是src/main/java)。它将带有lombok注释的java源代码放在src/main/lombok路径下,而不是src/main/java。因此,我创建了一个lombok文件夹并将UserInfo.java文件移入其中。然后执行maveninstall操作,可以看到在target/generated-sources/delombok路径下多了一个UserInfo.java文件:这个文件就是delombok插件处理的java文件,可以在对方没有使用lombok插件接下来,直接放到api里提供。然后我们看一下文件。拿到这个文件主要是想看看它的hashCode方法长什么样子:看看有没有看到,hashCode方法中的intPRIME=true没有了,取而代之的是finalintPRIME=59。这已经是一个java文件。如果这个地方还是正确的,那就是正常的编译错误:而delombok生成的源码也回答了我之前的一个问题:看class文件的时候,感觉变量PRIME没有用到。那么它的意义是什么?但是看delombok编译的java文件,才知道其实是用了PRIME:那么为什么PRIME变成了true呢?看着delombok生成的源码,顿时眼睛一亮,小子,看看这是什么:这是一个final类型的局部变量。注意:是的!最终的!种类!类型!为了更好的引出我下面要讲的概率,先给大家写一个很简单的事情:你看到没有,why和mx都变成了true,相当于直接把测试方法修改成这样:publicinttest(){return3;}把字节码给大家看可能更直观:左边是withoutfinal,右边是final。可以看到,加上final之后,根本就没有访问局部变量的iload操作了。这东西叫什么?这就是“不断折叠”。幸运的是,很早以前就看到了JVM老大R大对这个现象的解读。我当时觉得很有意思,所以有点印象。当我看到finalintPRIME=59的时候,我立马点燃了内存。于是找到了之前看到的链接:https://www.zhihu.com/questio...R大的回答里面有这么短的一段话,给大家截图一下:同时,我给大家展示下常量变量Java语言规范中对这个东西的定义:原始类型或者String类型的变量,是final的,用编译期常量表达式初始化的,称为常量变量。原始类型或String类型的变量,如果是Final修饰的,则在编译时完成初始化,称为常量变量(constantvariable)。所以finalintPRIME=59中的PRIME是一个常量变量。既然这里提到了String,那我就举个例子吧:看test2这个方法,它使用了final,在final的class文件中,直接返回了拼接后的字符串。为什么?不要问,问是规矩。https://docs.oracle.com/javas...这里只是给大家介绍一下方法,有兴趣的可以自行查阅。另外,我再次锤了下面class文件的显示,确实是idea的bug,和lombok完全没有关系,因为我这里根本不用lombok:同时,在lombok的githubDiscussion中有关于上述问题的相关问题:https://github.com/projectlom...提问者说:这个PRIME变量看起来像无用代码,因为在这个本地方法中没有用到.官方的回答是:兄弟,我怀疑你看到的是javac的优化。如果您查看delombok生成的代码,您会看到正在使用的PRIME东西。应该是javac在内联这个常量。为什么是59?我们再关注一下delombok生成的hashCode方法:这里为什么用59?hashCode里面的factor不应该想都没想就用31吗?我以为这里有故事,所以我又挖了一点。我挖掘线索的方式是这样的。首先,我要弄清楚59这个数字是怎么来的。它必须来自lombok中的文件。然后我拉下了lombok的源代码,在相应的文件中检查这个值的提交或更改。一般情况下,这个魔法值是不会无缘无故来的。提交代码的时候,大概率会给出解释为什么取这个值。我只需要找到那个描述。首先,我根据调用@EqualsAndHashCode的地方找到了这个类:lombok.javac.handlers.HandleEqualsAndHashCode然后在这个类中,你可以看到熟悉的“PRIME”:然后,搜索这个关键字,我找到了这个地方:Thismethodhere是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中,我发现了Newclueissue#660:Openissue#660看一看,嗯,这次应该是正确的方法:https://github.com/projectlom...在这个issues中,Maaartinus大哥先给了一段代码,然后他解释道:在我的举个例子,如果lombok生成的hashCode方法使用了31的因子,对于256个生成的对象,只有64个唯一的hash值,这意味着会产生大量的碰撞。但是如果lombok使用了更好的factor,这个数字增加到144,还是比较不错的。几乎任何奇数都可以。使用31是为数不多的错误选择之一。官方看到后连忙回复:看了大哥的程序,觉得大哥说的有道理。之前用31是因为没有考虑太多在《Effective Java》中建议的。此外,我决定使用数字277而不是31作为新因子。为什么是277?不问,问就很幸运!277是幸运儿,为什么最后从277变成了59呢?因为使用227这样一个“巨大”的因子,会有大约1-2%的性能损失。所以需要更改一个数字。最终决定选择59,虽然我没说具体原因:但是结合changelog,我有理由猜测其中一个原因是选择了一个小于127的数字,因为-128到127在Integer的缓存范围:IDEA说说IDEA的BUG,我早年踩过一个印象深刻的“BUG”。之前在调试ConcurrentLinkedQueue的时候,心态崩了。你可能会遇到一个巨大的坑,比如我们的测试代码是这样的:队列。报价(新对象());}}很简单,向队列中添加一个元素。在初始化head=tail=newNode
