Java中的Object类提供的equals方法接收Object类型参数,这使得在编译阶段无法校验传入参数的类型,即使传入完全不相关的对象类型,代码也能正常编译,只有在运行时才会根据业务逻辑返回比较结果,很容易引发隐藏的问题。

默认equals方法的问题示例
我们先来看一段常见的代码,展示默认equals的类型不安全问题:
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return age == user.age && name.equals(user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public static void main(String[] args) {
User user = new User("张三", 20);
// 编译通过,但是逻辑上没有比较意义,运行时返回false
boolean result = user.equals("张三");
System.out.println(result);
}
}
上面的代码中,User对象和字符串做equals比较,编译阶段不会有任何错误提示,但是实际上这种比较是没有业务意义的,很容易因为开发者的疏忽引入问题。
方案一:使用泛型约束实现编译时类型检查
我们可以通过泛型来约束equals方法的参数类型,让编译器在编译阶段就校验参数类型是否匹配。
实现思路
定义一个泛型接口,泛型参数限定为当前类的类型,接口中声明接收泛型类型参数的equals方法,然后让需要比较的类实现这个接口。
代码示例
// 泛型接口,T限定为实现类的类型
interface TypeSafeEquals<T extends TypeSafeEquals<T>> {
boolean typeSafeEquals(T other);
}
public class Student implements TypeSafeEquals<Student> {
private String studentId;
private String studentName;
public Student(String studentId, String studentName) {
this.studentId = studentId;
this.studentName = studentName;
}
@Override
public boolean typeSafeEquals(Student other) {
if (other == null) return false;
return studentId.equals(other.studentId) && studentName.equals(other.studentName);
}
public static void main(String[] args) {
Student s1 = new Student("1001", "李四");
Student s2 = new Student("1001", "李四");
// 编译通过,类型匹配
boolean r1 = s1.typeSafeEquals(s2);
// 编译报错,类型不匹配,String不能转换为Student
// boolean r2 = s1.typeSafeEquals("1001");
}
}
这种方案的优势是实现简单,不需要依赖第三方库,编译阶段就能拦截类型不匹配的比较。缺点是每个需要类型安全比较的类都需要实现对应的接口,并且需要额外定义typeSafeEquals方法,不能直接替换原有的equals方法使用。
方案二:自定义类型安全比较工具类
如果不想修改原有类的结构,可以定义一个通用的工具类,通过泛型约束来实现类型安全的比较。
代码示例
public class EqualsUtil {
// 泛型方法,要求两个参数类型一致
public static <T> boolean safeEquals(T obj1, T obj2) {
if (obj1 == obj2) return true;
if (obj1 == null || obj2 == null) return false;
return obj1.equals(obj2);
}
}
public class Order {
private String orderNo;
private double amount;
public Order(String orderNo, double amount) {
this.orderNo = orderNo;
this.amount = amount;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Order order = (Order) obj;
return Double.compare(order.amount, amount) == 0 && orderNo.equals(order.orderNo);
}
@Override
public int hashCode() {
return Objects.hash(orderNo, amount);
}
public static void main(String[] args) {
Order o1 = new Order("ORD001", 100.0);
Order o2 = new Order("ORD001", 100.0);
// 编译通过,类型匹配
boolean r1 = EqualsUtil.safeEquals(o1, o2);
// 编译报错,类型不匹配,Order和String类型不同
// boolean r2 = EqualsUtil.safeEquals(o1, "ORD001");
}
}
这种方案的优势是使用灵活,不需要修改原有类的代码,只需要调用工具类的静态方法即可。缺点是如果开发者忘记使用工具类,还是可能调用默认的equals方法,无法从根源上避免问题。
方案三:使用第三方库实现
一些第三方工具库已经提供了类型安全的比较能力,比如Apache Commons Lang或者Guava,我们可以借助这些库的能力简化实现。
Guava的Objects.equal方法
Guava的Objects.equal方法本身不会做编译时类型检查,但是结合泛型使用可以实现类似的效果,不过更推荐的是使用Guava的Equivalence类来实现自定义的类型安全比较逻辑。
import com.google.common.base.Equivalence;
public class Product {
private String productId;
private String productName;
public Product(String productId, String productName) {
this.productId = productId;
this.productName = productName;
}
// 定义Product的等价性规则
public static final Equivalence<Product> PRODUCT_EQUIVALENCE = new Equivalence<Product>() {
@Override
protected boolean doEquivalent(Product a, Product b) {
if (a == b) return true;
if (a == null || b == null) return false;
return a.productId.equals(b.productId) && a.productName.equals(b.productName);
}
@Override
protected int doHash(Product product) {
return Objects.hash(product.productId, product.productName);
}
};
public static void main(String[] args) {
Product p1 = new Product("P001", "手机");
Product p2 = new Product("P001", "手机");
// 编译通过,类型匹配
boolean r1 = PRODUCT_EQUIVALENCE.equivalent(p1, p2);
// 编译报错,类型不匹配
// boolean r2 = PRODUCT_EQUIVALENCE.equivalent(p1, "P001");
}
}
第三方库的优势是功能成熟,经过了大量场景的验证,同时提供了更多的扩展能力。缺点是需要引入额外的依赖,如果项目本身没有使用这些库,会增加项目的依赖体积。
不同方案对比
我们可以通过下面的表格对比不同方案的优缺点,方便选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 泛型接口约束 | 无额外依赖,编译时强制检查 | 需要修改类结构,额外定义方法 | 新开发的类,对类型安全要求高的场景 |
| 工具类方法 | 无需修改原有类,使用灵活 | 依赖开发者主动使用,无法强制约束 | 已有大量存量类,不想修改类结构的场景 |
| 第三方库实现 | 功能成熟,扩展性强 | 需要引入额外依赖 | 项目已经使用对应第三方库的场景 |
注意事项
- 实现编译时类型安全的equals比较时,不要忘记同时重写hashCode方法,保证两个相等的对象hashCode也相等,避免在使用HashMap、HashSet等集合时出现逻辑错误。
- 泛型约束的方案中,要注意泛型递归边界的写法,避免泛型定义错误导致约束失效。
- 如果使用工具类方案,建议在团队内部规范中明确要求使用工具类进行比较,减少人为疏忽。