Object.equals方法:重载还是覆盖

本文深入探讨Java中equals方法的正确实现方式,解释为何重载equals方法会导致问题,并提供正确的覆盖equals方法的示例。

本文译自StackOverflow上对此问题的讨论。

原问题链接

 

在阅读Joshua Bloch的《Effective Java(第二版)》第8条“覆盖equals时请遵守通用约定”时对如下论述有疑问:

“不要将equals声明中的Object对象替换为其他的类型。程序员编写出下面这样的equals方法并不鲜见,这会使程序员花上数个小时都搞不清它为什么不能正常工作:”

public boolean equals(MyClass o) {
    //...
}

 “问题在于,这个方法并没有覆盖(override)Object.equals,因为它的参数应当是Object类型,相反,它重载(overload)了Object.equals。”

 

问题:

为何代码示例中的强类型的equals方法重载并不足够?书中提到重载而非覆盖会引起问题,但并未论述为何如此也没有说明在何种场景下会使得equals方法失败。

 

回答:

这是因为重载此方法并不会改变集合类或者其他地方显式调用equals(Object)的行为。例如:

public class MyClass {

    public boolean equals(MyClass m) {
        return true;
    }
}

如果把它放到HashSet中:

public static void main(String[] args) {
    Set<MyClass> myClasses = new HashSet<>();
    myClasses.add(new MyClass());
    myClasses.add(new MyClass());
    System.out.println(myClasses.size());
}

上面程序将会打印出2,而不是1。虽然你期望所有的MyClass实例经由重载方法判断都是相等,并且集合不会添加第二个实例。

所以基本上,即使下面表达式为true:

MyClass myClass = new MyClass();
new MyClass().equals(myClass);

 下述表达式依然为false:

Object o = new MyClass();
new MyClass().equals(o);

后一个表达式是集合或其他类用于判断相等性的。事实上,只有当参数显式地为MyClass或其子类型的实例时,才会调用到重载方法并返回true。

 

关于覆盖还是重载的问题:

让我们从覆盖和重载的区别说起。通过覆盖,你事实上重新定义了这个方法。事实上相当于你删除了方法原始的实现并替换为自己的实现。所以当你这样做时:

@Override
public boolean equals(Object o) { ... }

你事实上重新链接了你的equals实现以取代Object类(或者实现该方法的最后一个父类)中的实现。

另一方面,当你这样做:

public boolean equals(MyClass m) { ... }

你定义了一个全新的方法,因为你定义了一个拥有同样名字但是不同参数列表的方法。当HashSet调用equals时,它调用的是参数类型为Object的方法。

Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

 (上述代码来自HashMap.put,被用作HashSet.add的底层实现。)

为了更清楚,MyClass中的equals方法只有当被覆盖时才会被调用,而不是被重载的时候。如果你试图在一个重载的equals方法上添加@Override注解,它将会产生一个编译错误,指出它并没有覆盖一个方法。我们可以在一个类中声明两个equals方法,因为这是重载:

public class MyClass {

    @Override
    public boolean equals(Object o) {
        return false;
    }

    public boolean equals(MyClass m) {
        return true;
    }
}

 

泛型

谈到泛型,equals方法并不是泛型。它显式地要求Object作为它的参数类型。当你试图这样做时:

public class MyGenericClass<T> {

    public boolean equals(T t) {
        return false;
    }
}

它将不会编译,错误信息:命名冲突,MyGenericClass的equals(T)方法类型擦除后与Object类中equals(Object)相同,但并未覆盖它

Name clash: The method equals(T) of type MyGenericClass has the same erasure as equals(Object) of type Object but does not override it

 当添加@Override注解时:

public class MyGenericClass<T> {

    @Override
    public boolean equals(T t) {
        return false;
    }
}

 错误信息变为:MyGenericClass的equals(T)方法必须覆盖或实现父类方法

The method equals(T) of type MyGenericClass must override or implement a supertype method

于是怎么做都会有问题。原因在于Java通过类型擦除实现泛型。当Java在编译阶段检查完所有的泛型类型,事实上的运行时对象都会被Object取代。无论何时你看到T类型,事实上的字节码都会包含Object。这就是为何反射不能用于泛型类型以及list instanceof List<String>将会出错的原因。

同样,这也使你无法重载泛型类型,如果有这样的类:

public class Example<T> {
    public void add(Object o) { ... }
    public void add(T t) { ... }
}

add(T)方法将会产生编译错误,因为类完成编译时,两个方法将会有同样的签名,public void add(Object)。

```java /* * 代码概述: * 本代码实现了一个工具类 DataAnalyzer,提供两个核心功能:数值求和与去重统计。 * 充分使用了可变长参数(varargs)特性,并处理了空输入、类型不一致、非Number对象等边界情况。 */ public class DataAnalyzer { // 方法1:可变长整数求和(返回long避免int溢出) public static long sum(int... nums) { if (nums == null || nums.length == 0) return 0; long total = 0; for (int num : nums) { total += num; } return total; } // 方法1:可变长浮点数求和(返回double) public static double sum(double... nums) { if (nums == null || nums.length == 0) return 0.0; double total = 0.0; for (double num : nums) { total += num; } return total; } // 方法1:可变长Object数组中提取Number类型求和(忽略非Number) public static double sum(Object... objs) { if (objs == null || objs.length == 0) return 0.0; double total = 0.0; boolean hasValidNumber = false; for (Object obj : objs) { if (obj instanceof Number) { total += ((Number) obj).doubleValue(); hasValidNumber = true; } } return hasValidNumber ? total : 0.0; } // 方法2:统计可变长Comparable元素中不重复项的个数(使用compareTo判断相等性) public static int distinctCount(Comparable... elements) { if (elements == null || elements.length == 0) return 0; // 检查类型一致性:所有元素必须是同一类型 Class<?> clazz = elements[0].getClass(); for (Comparable elem : elements) { if (elem == null) continue; // 忽略null元素(可按需扩展) if (!elem.getClass().equals(clazz)) { System.err.println("元素类型必须一致"); return 0; } } // 使用冒泡式比较统计不同元素个数(基于compareTo结果为0表示相等) int distinct = 0; boolean isUnique; for (int i = 0; i < elements.length; i++) { if (elements[i] == null) continue; isUnique = true; for (int j = 0; j < i; j++) { if (elements[j] != null && elements[i].compareTo(elements[j]) == 0) { isUnique = false; break; } } if (isUnique) distinct++; } return distinct; } // 测试主类 public static void main(String[] args) { // 测试 sum(int...) System.out.println("sum(int...): " + DataAnalyzer.sum(1, 2, 3, 4)); // 10 // 测试 sum(double...) System.out.println("sum(double...): " + DataAnalyzer.sum(1.5, 2.5, 3.0)); // 7.0 // 测试 sum(Object...) —— 包含Integer, Double, String等 System.out.println("sum(Object...): " + DataAnalyzer.sum(10, 2.5, "hello", true, 3L)); // 15.5 // 测试空输入 System.out.println("sum() empty: " + DataAnalyzer.sum()); // 0 // 测试 distinctCount(Comparable...) System.out.println("distinctCount(String): " + DataAnalyzer.distinctCount("apple", "banana", "apple")); // 2 System.out.println("distinctCount(Integer): " + DataAnalyzer.distinctCount(1, 2, 2, 3)); // 3 // 测试混合类型(应报错) Object[] mixed = { "hello", 123 }; Comparable[] comp = new Comparable[mixed.length]; System.arraycopy(mixed, 0, comp, 0, mixed.length); DataAnalyzer.distinctCount(comp); // 输出:元素类型必须一致 } } ``` ### **代码解析** - **sum(int...)**:安全累加到 `long` 类型,防止大整数溢出;判空后遍历求和。 - **sum(double...)**:直接以 `double` 累加,支持小数运算。 - **sum(Object...)**:逐个判断是否为 `Number` 子类实例(如 `Integer`、`Double`),仅对有效数字求和并转为 `double` 返回。 - **distinctCount(Comparable...)**:先校验所有非空元素类型是否一致,再通过 `compareTo()` 判断唯一性,避免使用 `equals`。 - 所有方法均处理 `null` 或长度为0的情况,确保鲁棒性。 ### **知识点** 1. **可变长参数(varargs)**:用`...`定义灵活参数列表,调用时可传入多个同类型值。 2. **instanceof 与类型转换**:用于在Object数组中识别Number类型并安全取值。 3. **Comparable接口与compareTo**:通过`compareTo==0`判断两对象逻辑相等,实现去重统计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值