【Java】@EqualsAndHashCode 注解解析

这是 Lombok 框架的核心注解之一,作用是自动帮你生成 equals()hashCode() 方法。

一、注解核心作用

1. 解决的问题

Java 中如果需要自定义对象的相等性判断,必须手动重写 equals()hashCode() 方法,不仅代码冗长,还容易出错(比如漏字段、逻辑不一致)。

@EqualsAndHashCode 会在编译期自动生成这两个方法,完全遵循 Java 规范:

  • **equals():**按类的字段逐一比较,判断两个对象是否相等
  • hashCode() 基于相同字段生成哈希值,保证 a.equals(b) == truea.hashCode() == b.hashCode()

2. 源码分析

从源码可以看到:

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE) // 源码级注解,仅编译期生效
public @interface EqualsAndHashCode {
    // 核心参数
    String[] exclude() default {};    // 排除指定字段,不参与比较
    String[] of() default {};          // 仅包含指定字段,参与比较
    boolean callSuper() default false; // 是否调用父类的equals/hashCode
    // ...其他参数
}
  • @Retention(SOURCE) 仅在源码阶段生效,编译成 .class 文件后就会被擦除,不影响运行期性能
  • **@Target(TYPE):**只能标注在类上

二、代码中 callSuper = true 的关键作用

java 复制代码
@EqualsAndHashCode(callSuper = true)
public class PTS1400106039In extends PtRequest { ... }

1. 核心含义

  • **默认 callSuper = false:**生成的 equals()/hashCode() 只比较当前类的字段,完全忽略父类 的字段
  • callSuper = true 生成的方法会先调用父类的 equals()/hashCode(),再比较当前类的字段,保证父类字段也参与相等性判断

2. 注意

如果你的类继承了父类,且父类有自己的业务字段,必须设置 callSuper = true,否则会出现以下的严重逻辑错误:

  • 两个子类对象,父类字段不同、子类字段相同,会被错误判定为相等
  • 哈希值只基于子类字段生成,导致 HashSet/HashMap 存储异常

三、常用参数详解

参数 作用 示例
callSuper 是否调用父类方法,继承类必须设为 true @EqualsAndHashCode(callSuper = true)
exclude 排除指定字段,不参与比较 @EqualsAndHashCode(exclude = {"id", "createTime"})
of 仅包含指定字段,其他字段全部忽略 @EqualsAndHashCode(of = {"orderNo", "userId"})
doNotUseGetters 直接访问字段,不调用 getter 方法(默认 false,调用 getter) @EqualsAndHashCode(doNotUseGetters = true)
cacheStrategy 哈希值缓存策略,LAZY 表示首次计算后缓存 @EqualsAndHashCode(cacheStrategy = CacheStrategy.LAZY)

四、和 @Data 的关系

为什么要同时用 @Data @EqualsAndHashCode(callSuper = true)这里有一个关键点:

  • @Data 注解默认包含 @EqualsAndHashCode,但默认 callSuper = false
  • 当类继承了父类时,@Data 生成的方法会忽略父类字段,必须显式添加 @EqualsAndHashCode(callSuper = true) 覆盖默认行为!

五、实际效果示例

原始手写代码(冗余易错)↓

java 复制代码
public class PTS1400106039In extends PtRequest {
    private String field1;
    private Integer field2;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false; // callSuper = true 才会有这行
        PTS1400106039In that = (PTS1400106039In) o;
        return Objects.equals(field1, that.field1) && Objects.equals(field2, that.field2);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), field1, field2); // callSuper = true 才会包含父类哈希
    }
}

Lombok 注解(一行搞定)↓

java 复制代码
@Data
@EqualsAndHashCode(callSuper = true)
public class PTS1400106039In extends PtRequest {
    private String field1;
    private Integer field2;
}

编译后自动生成和上面完全一致的代码,彻底消除冗余。

六、举例说明区别

1. 准备父类和子类

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;

// 父类:包含 id 字段
@Data
@AllArgsConstructor
class BaseEntity {
    private Long id;
}

// 子类:包含 name 字段,继承 BaseEntity
@Data
// @EqualsAndHashCode(callSuper = true) // 先注释,测试默认行为
public class User extends BaseEntity {
    private String name;

    // 构造器:给 id 和 name 赋值
    public User(Long id, String name) {
        super(id);
        this.name = name;
    }
}

2. 默认情况测试(不加 callSuper = true)

测试代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 两个对象:name相同,id不同
        User user1 = new User(1L, "张三");
        User user2 = new User(2L, "张三");

        System.out.println("user1.equals(user2) = " + user1.equals(user2));
        System.out.println("user1.hashCode() = " + user1.hashCode());
        System.out.println("user2.hashCode() = " + user2.hashCode());
    }
}

运行结果:

复制代码
user1.equals(user2) = true
user1.hashCode() = 24021040
user2.hashCode() = 24021040

原因:

默认情况下:

  • Lombok 只用子类 Username 字段生成 equals()hashCode()
  • 完全忽略父类 BaseEntityid 字段
  • 所以即使 id 不同,只要 name 相同,就判定为相等(错误)

3. 加上注解后测试(不加 callSuper = true)

打开子类注解,再次运行测试代码:

java 复制代码
@Data
@EqualsAndHashCode(callSuper = true) // 开启父类判断
public class User extends BaseEntity {

运行结果:

java 复制代码
user1.equals(user2) = false
user1.hashCode() = 24021041
user2.hashCode() = 24021042

原因:

加了 callSuper = true

  • Lombok 会同时使用子类 name + 父类 id 生成方法
  • 必须所有字段都相同,才判定为相等(正确)

核心区别总结

注解方式 行为 适用场景
@EqualsAndHashCode(默认) 只比较当前类字段,忽略父类 类没有继承关系,或继承自 Object
@EqualsAndHashCode(callSuper = true) 比较当前类 + 父类所有字段 有继承关系,父类有业务字段(最常用)

七、建议

  1. 继承类必须加 callSuper = true :只要类 extends 了其他类,就必须显式设置,否则父类字段不参与比较
  2. 避免和 @Data 冲突@Data 自带 @EqualsAndHashCode,如果需要自定义参数,必须显式覆盖
  3. 谨慎使用 exclude/of:只在明确不需要比较的字段(如 transient、临时字段)时使用,避免漏判
  4. @ToString 保持一致 :如果用了 @ToString,建议同步设置 @ToString(callSuper = true),保证日志打印完整

八、HashCode计算

Lombok 生成的 hashCode = 所有参与计算的字段 按规则混合算出的数字


先看 Lombok 生成 hashCode 的固定算法

Lombok 统一用这个公式计算 hashCode(简化版):

java 复制代码
// 伪代码
public int hashCode() {
    // 初始值
    int result = 1;
    
    // 遍历所有【参与计算的字段】
    for (字段 : 参与字段) {
        // 用字段值混合计算
        result = result * 31 + 字段.hashCode();
    }
    
    // 最终返回这个数字
    return result;
}
  • 正向 :如果 hashCode 不相等 → 则 参与字段一定不同。(这是成立的,因为如果相同,hash 必须一样)
  • 反向 :如果 参与字段不同hashCode 不一定不同。(可能碰撞)
  • 反向特例 :如果 参与字段相同hashCode 一定相同。(这是必须遵守的契约)
相关推荐
卖芒果的潇洒农民2 小时前
C语言面试
c语言·开发语言·面试
StackNoOverflow2 小时前
Spring Data Redis 详解
java·redis·spring
人间打气筒(Ada)2 小时前
「码动四季·开源同行」go语言:如何追踪分布式系统调用链路的问题?
开发语言·golang·开源·分布式链路追踪
前进的李工2 小时前
SSH隧道实现Dify与Ollama跨服务器通信
开发语言·大模型·github·腾讯云·autodl·dify
2401_840192272 小时前
数据库连接池和java servlet
java·数据库·servlet
OtIo TALL2 小时前
Spring Boot管理用户数据
java·spring boot·后端
小碗羊肉2 小时前
【从零开始学Java | 第二十五篇】Set集合
java·开发语言
551只玄猫2 小时前
【数学建模 matlab 实验报告4】非线性规划作业
开发语言·数学建模·matlab·课程设计·实验报告
Seven972 小时前
用300行代码手写Spring核心原理
java