引言

今天中午正在带着耳机遨游在代码的世界里,被运营在群里​​@​​了,气冲冲的反问我最近有删生产的用户数据的吗?我肯定客气的回答道没有呀?生产的数据我怎么能随随便便可以删除,这可是公司的红线,再说了我也没有数据库的删除权限啊,不过查询权限还是有的。赶紧登上堡垒机,然后去生产数据库查一下数据,查了一下数据是还在的,吓死了,数据还在问题就不大了,无非就是应用程序出问题了,赶紧打开代码查看下,为什么会少了一条用户数据,看了下代码貌似没啥问题就是比较简单的一个逻辑,直接从​​DB​​通过分页查询数据给到前端,然后前端负责展示,没有啥复杂的逻辑。心想肯定是前端的问题,肯定是他少展示了数据,立马把问题也甩给了他,让他帮忙配合一起看看是否是前端的问题,然后自己也仔细看看代码,不到一分钟前端说他展示的数据没有问题,都是后端给到的,没有漏掉展示的。那就是后端的​​bug​​了罗。肉眼望去觉得可能出问题的就是分页导致的数据丢了。不过这个分页插件是全公司都在用,应该不至于出问题吧,找不到问题只能让测试帮忙在测试环境试试,看看是否可以复现。

测试环境复现

仔细看了一眼,居然有个去重的方法,去重逻辑也比较简单就是把​​list​​通过转为set去下重,看下来应该就是这个去重方法有问题了大致写了单元测试模仿了下生产的数据,大致逻辑如下:

public static void main(String[] args) {
Set<UserDTO> userSet = new HashSet<>();
UserDTO userDTO = new UserDTO();
userDTO.setId(1);
userDTO.setUserName("java金融");
UserDTO userDTO1 = new UserDTO();
userDTO1.setId(2);
userDTO1.setUserName("java金融");
userSet.add(userDTO);
userSet.add(userDTO1);
System.out.println(userSet.size());
System.out.println(userDTO1.equals(userDTO));
}
@Data
static class UserDTO extends BaseDTO {
private String userName;
}
@Data
static class BaseDTO {
private Integer id;
}

我们可以输出结果​​set​​集合的长度是1,​​user1​​和​​user2​​ 是相等的,明明两个​​user​​的​​ID​​是不一样的,为何会相等,我们知道set可以去重是因为Set的操作,都是通过操作map来实现的,


​set​​的​​add​​其实就是调用​​map​​的​​put​​方法,​​map​​的​​put​​方法我相信大家应该都去看过其源码,这里就不详细再说了,大概流程就是通过key通过hash算法定位到数组的下标,先判断​​key​​的​​hash​​是否相等,如果相等再去判断key的value相等,如果都相等就会覆盖原来的值。我们上面这个例子就是对象的​​hash​​和​​value​​都相等导致,但是我们的两个对象​​user1​​和​​user2​​ 应该是不等的,因为ID不等,那为什么会相等列?我们仔细看下上面的代码,我们使用了​​lombok​​里面​​@Data​​​注解,我们可以看看这个注解帮我们生成了哪些方法一不小心就踩了lombok的坑?_java一不小心就踩了lombok的坑?_数据_02通过上面的对比我们可以看出​​​@Data​​注解帮我们生成了 注在类上,提供类的​​get、set、equals、hashCode、canEqual、toString​​方法,这个注解确实比较方便。上面那个​​bug​​ 就是因为它生成的equals方法有问题,我们可以把上述代码编译下,然后把​​class​​ 里面生成的​​equals​​​方法拷贝出来看看一不小心就踩了lombok的坑?_数据_03通过上述生成的代码我们可以看出​​​equals​​方法只比较了​​userName​​这个字段,也就是当前类的字段,并没有去比较父类的字段,这就是导致两个对象相等的原因,我们既然找到问题了,那解决问题就比较简单。

解决问题


  • 手动重写equals
    和hashCode
    方法,这种方法肯定是不推荐的,我们既然用了lombok
    就是为了解放我们的双手,是代码变得更加简洁。
  • 在比较的类上加上@EqualsAndHashCode(callSuper = true) callSuper = true
    会包含父类的equals
    和 hashCode
    方法我们可以对比下加上@EqualsAndHashCode(callSuper = true)
    和没有加上这个注解生成的equals
    方法的代码差异。一不小心就踩了lombok的坑?_父类_04差异点还是很明显的,加入了@EqualsAndHashCode(callSuper = true)
    会去调用父类的equals
    方法比较,所以这个注解也能够解决这个问题。
  • 这样加上注解确实可以解决问题,但是每个类上面都要加上这个注解,这也是个体力活。我们可以再找找其他的方案,例如有没有比如配置文件设置下什么的,然后就能全局生效了。最终通过查询资料发现我们我们写一个lombok.config
    的配置文件放在我们项目的根目录下面,内容写上lombok.equalsAndHashCode.callSuper = call
    效果等同于@EqualsAndHashCode(callSuper = true)
    ,这样的话我们就不需要为每个类都去加上这个注释了,相当于在这个项目下面只要用到了@Data
    注解的类都会为其加上@EqualsAndHashCode(callSuper = true)
    通过配置文件的方式就可以全局生效。

总结


  • 我们再来回顾下上面的问题,归根结底还是由于对象的​​equals​​​方法使用不当引起的,所以我们在如果在判断自定义对象业务判断相等的时候需要去重写下​​hashCode​​​和​​equals​​​方法,重写的时候我们可以通过​​idea​​来生成,生成后最好还是去看一眼,看看生成的是否符合我们的业务要求。
  • 我们在工作中操作一些常见的容器类比如Set、Map等来实现一些我们自己的业务,我们还是有必要去看看它们的源码的,就比如我们通过Set来进行去重,如果我们是使用的自定义对象的话,如果没有重写​​hashCode​​​和​​equals​​​方法的话,去重就会去不成功,我们只有了解了它,才能真正的去用好它。在关于​​hashCode​​​和​​equals​​​ 阿里巴巴开发手册也有明确的说到一不小心就踩了lombok的坑?_父类_05
  • ​lombok​​​ 用起来还是挺爽的,但是还是有一些细节需要稍微注意下。使用前可以大概的去看看它的官网提供的内容,不然出现莫名其妙的问题你都不知道如何下手。这个就有点类似于我们使用​​SpringBoot​​​一样,用起来非常爽,但是如果遇到莫名其妙的​​bug​​解决起来就比较头疼。
  • 最后我们再来回顾几道关于​​hashCode​​​和​​equals​​​的比较常见的面试题?其实如果我们只要真正看过HashMap的源码的话,这下面几个面试题还是非常简单的。什么情况下需要我们去重写 方法?如果只重写​​equals​​​方法不重写​​HashCode​​​可以吗?​​equals ,== 和hashcode()​​的区别?