1. 概述

本文将深入探讨 Lombok 的 @EqualsAndHashCode 注解,该注解能基于类的字段自动生成 equals()hashCode() 方法。对于有经验的 Java 开发者来说,手动实现这两个方法既繁琐又容易出错,Lombok 提供了这个注解来简化开发流程。

2. @EqualsAndHashCode 基本用法

@EqualsAndHashCode 注解会默认使用所有非静态、非瞬态字段生成 equals()hashCode() 方法。如果需要自定义字段范围,可以通过以下方式控制:

  • 使用 @EqualsAndHashCode.Include 显式包含字段
  • 使用 @EqualsAndHashCode.Exclude 显式排除字段

来看个简单例子:

@EqualsAndHashCode
public class Employee {
    private String name;
    private int id;
    private int age;
}

编译后 Lombok 会自动生成以下方法(反编译代码):

public class EmployeeDelomboked {
    private String name;
    private int id;
    private int age;

    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Employee)) return false;
        Employee other = (Employee) o;
        if (!other.canEqual(this)) return false;
        if (this.getId() != other.getId()) return false;
        if (this.getAge() != other.getAge()) return false;
        Object this$name = this.getName();
        Object other$name = other.getName();
        if (this$name == null) {
            if (other$name == null) return true;
        } else if (this$name.equals(other$name)) {
            return true;
        }
        return false;
    }

    protected boolean canEqual(Object other) {
        return other instanceof Employee;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        result = result * PRIME + this.getId();
        result = result * PRIME + this.getAge();
        Object $name = this.getName();
        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
        return result;
    }
}

⚠️ 注意:Lombok 在生成方法时会额外创建 canEqual() 方法,用于确保比较对象是当前类的实例,避免子类破坏 equals 约定。

3. 排除特定字段

3.1. 字段级别排除:@EqualsAndHashCode.Exclude

在字段上直接使用 @EqualsAndHashCode.Exclude 注解,可将其从生成逻辑中排除:

@EqualsAndHashCode
public class Employee {
    private String name;

    @EqualsAndHashCode.Exclude
    private int id;

    @EqualsAndHashCode.Exclude
    private int age;
}

生成的 hashCode() 方法将只使用 name 字段:

public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final Object $name = this.getName();
    result = result * PRIME + ($name == null ? 43 : $name.hashCode());
    return result;
}

3.2. 类级别排除

在类注解中通过 exclude 属性指定要排除的字段:

@EqualsAndHashCode(exclude = {"id", "age"})
public class Employee {
    private String name;
    private int id;
    private int age;
}

效果与字段级排除完全相同,但更集中管理。

4. 包含特定字段

4.1. 字段级别包含:@EqualsAndHashCode.Include

需要显式指定包含字段时,配合 onlyExplicitlyIncluded = true 使用:

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Employee {
    @EqualsAndHashCode.Include
    private String name;
    
    @EqualsAndHashCode.Include
    private int id;
    
    private int age; // 未标注,自动排除
}

✅ 只有被 @EqualsAndHashCode.Include 标注的字段会参与计算。

4.2. 方法级别包含

可以将方法返回值纳入计算逻辑:

@EqualsAndHashCode
public class Employee {
    private String name;
    private int id;
    private int age;

    @EqualsAndHashCode.Include
    public boolean hasOddId() {
        return id % 2 != 0;
    }
}

生成的 hashCode() 会包含方法结果:

public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    result = result * PRIME + this.getId();
    result = result * PRIME + this.getAge();
    result = result * PRIME + (this.hasOddId() ? 79 : 97); // 方法结果参与计算
    final Object $name = this.getName();
    result = result * PRIME + ($name == null ? 43 : $name.hashCode());
    return result;
}

4.3. 类级别包含

使用 of 属性指定包含字段(不推荐,未来可能废弃):

@EqualsAndHashCode(of = {"name", "id"})
public class Employee {
    private String name;
    private int id;
    private int age;
}

⚠️ 踩坑提示:Lombok 官方推荐使用 onlyExplicitlyIncluded 替代 of,后者未来可能被废弃。

5. 继承关系处理

默认情况下,子类的 @EqualsAndHashCode **不会调用父类的 equals()hashCode()**。要包含父类逻辑,需设置 callSuper = true

@EqualsAndHashCode(callSuper = true)
public class Manager extends Employee {
    private String departmentName;
    private int uid;
}

生成的 hashCode() 会调用父类方法:

public int hashCode() {
    final int PRIME = 59;
    int result = super.hashCode(); // 调用父类方法
    result = result * PRIME + this.getUid();
    final Object $departmentName = this.getDepartmentName();
    result = result * PRIME + ($departmentName == null ? 43 : $departmentName.hashCode());
    return result;
}

6. 全局配置

在项目根目录创建 lombok.config 文件可配置全局行为:

6.1. lombok.equalsAndHashCode.doNotUseGetters

lombok.equalsAndHashCode.doNotUseGetters = [true | false] (默认: false)
  • true:直接访问字段(即使存在 getter)
  • false:优先使用 getter

6.2. lombok.equalsAndHashCode.callSuper

lombok.equalsAndHashCode.callSuper = [call | skip | warn] (默认: warn)
  • call:子类自动调用父类方法
  • skip:不调用父类方法
  • warn:不调用但发出警告(默认行为)

6.3. lombok.equalsAndHashCode.flagUsage

lombok.equalsAndHashCode.flagUsage = [warning | error] (默认: 未设置)

设置为 warningerror 后,使用 @EqualsAndHashCode 会触发相应级别的提示。

7. 优缺点分析

7.1. 优势

减少样板代码:自动生成方法,避免手动编写 ✅ 高度可定制:灵活控制字段包含/排除 ✅ 降低出错风险:避免手动实现时的常见错误(如忘记更新 hashCode()

7.2. 劣势

哈希冲突风险:不同字段值可能产生相同哈希值 ❌ 框架兼容问题:某些依赖反射的框架可能无法识别生成的方法 ❌ 可读性下降:隐藏了方法实现细节,增加代码理解成本

8. 常见问题

双向关联导致的栈溢出

当类之间存在双向关联时,可能触发 StackOverflowError

@EqualsAndHashCode
public class Employee {
    private String name;
    private Manager manager; // 关联 Manager
}

@EqualsAndHashCode
public class Manager {
    private String name;
    private Employee assistantManager; // 关联 Employee
}

问题原因

  • Employee.hashCode()Manager.hashCode()Employee.hashCode() → 无限循环

解决方案: 排除其中一个关联字段:

@EqualsAndHashCode(exclude = "manager")
public class EmployeeV2 {
    private String name;
    private Manager manager;
}

9. 总结

Lombok 的 @EqualsAndHashCode 注解通过自动生成 equals()hashCode() 方法,显著简化了开发工作。主要要点:

  1. 默认使用所有非静态/非瞬态字段
  2. 灵活的字段控制:通过 Include/Exclude 精准定制
  3. 继承处理:使用 callSuper=true 包含父类逻辑
  4. 全局配置:通过 lombok.config 统一管理行为
  5. 避坑指南:双向关联时需排除字段避免栈溢出

虽然该注解能提升开发效率,但在复杂继承结构或需要特殊哈希算法的场景下,仍需谨慎评估其适用性。完整示例代码可在 GitHub 查看。


原始标题:Lombok EqualsAndHashCode Annotation | Baeldung