【问题标题】:Is overloading equals worthwhile重载是否值得
【发布时间】:2011-02-24 00:41:00
【问题描述】:

考虑以下 sn-p:

import java.util.*;
public class EqualsOverload {
    public static void main(String[] args) {
        class Thing {
            final int x;
            Thing(int x)          { this.x = x; }
            public int hashCode() { return x; }

            public boolean equals(Thing other) { return this.x == other.x; }
        }
        List<Thing> myThings = Arrays.asList(new Thing(42));
        System.out.println(myThings.contains(new Thing(42))); // prints "false"
    }
}

注意contains 返回false!!!我们好像丢了东西!!

错误当然是我们不小心重载,而不是覆盖Object.equals(Object)。如果我们将class Thing 改为如下所示,那么contains 会按预期返回true

        class Thing {
            final int x;
            Thing(int x)          { this.x = x; }
            public int hashCode() { return x; }

            @Override public boolean equals(Object o) {
                return (o instanceof Thing) && (this.x == ((Thing) o).x);
            }
        }

Effective Java 2nd Edition,Item 36: Consistently use the Override annotation,使用基本相同的论点来建议应该一致使用@Override。当然,这个建议很好,因为如果我们尝试在第一个 sn-p 中声明 @Override equals(Thing other),我们友好的小编译器会立即指出我们愚蠢的小错误,因为它是重载,而不是覆盖。

然而,本书没有具体介绍的是,重载equals 是否是一个好主意。基本上有3种情况:

  • 仅重载,无重载 -- 几乎肯定是错误的
    • 这基本上是上面的第一个 sn-p
  • 仅覆盖(无过载)- 一种修复方法
    • 这实际上是上面的第二个 sn-p
  • 重载和覆盖组合 -- 另一种修复方法

第三种情况用下面的sn-p来说明:

        class Thing {
            final int x;
            Thing(int x)          { this.x = x; }
            public int hashCode() { return x; }

            public boolean equals(Thing other) { return this.x == other.x; }
            @Override public boolean equals(Object o) {
                return (o instanceof Thing) && (this.equals((Thing) o));
            }
        }

这里,虽然我们现在有 2 个equals 方法,但仍然有一个相等逻辑,它位于重载中。 @Override 只是委托给重载。

所以问题是:

  • “仅覆盖”与“重载和覆盖组合”的优缺点是什么?
  • 是否有理由重载 equals,或者这几乎可以肯定是一种不好的做法?

【问题讨论】:

  • 我想说最好的做法是使用 eclipse 或类似的 IDE 来生成 equals 方法,只在必要时对生成的代码进行一些编辑。
  • 我认为最好的方法是(Eclipse 插件)Project Lombok 的 @EqualsAndHashCode 注释,默认情况下使用您的属性自动生成等号和哈希码(但可配置)。见projectlombok.org
  • @Tim Bender:eclipse 生成的代码简直太糟糕了。您至少必须使用 Guava 的 Objects.equals 之类的东西才能获得一些可读性。
  • @maaartinus,我不明白你的建议。 Guava 的 Objects.equal 函数仅仅依赖于有问题的类来正确实现 equals(Object) 方法。作为参考,请阅读无序列表后文档的最后一行:docs.guava-libraries.googlecode.com/git/javadoc/com/google/…
  • @Tim Bender:比较 this generated codethis one。通过使用单行代码equal,您可以消除大量空测试并使其全部可读。

标签: java equals overloading overriding


【解决方案1】:

我没有看到重载 equals 的情况,除了更容易出错且更难维护,尤其是在使用继承时。

在这里,保持自反性、对称性和传递性或检测它们的不一致可能非常困难,因为您必须始终注意实际调用的 equals 方法。想想一个大的继承层次结构,只有一些类型实现了它们自己的重载方法。

所以我会说不要这样做。

【讨论】:

  • 同意 - 超载会带来更多的工作量,而且对您的收益微乎其微。像new Thing(0).equals("lulz") 这样的东西仍然编译得很好,而且自从演员ThingObject 是隐式的,它不会“更容易”检查两个 Thing 实例 equals() 是否彼此。
  • 强烈不同意,因为重写的等号将是自反的、传递的和对称的,当且仅当重载的等号是。
  • @aioobe:如果Base添加了重载equals(Base),并覆盖equals(Object)调用重载,如果Derived覆盖equals(Object)但不覆盖equals(Base),以及如果d1d2 都是Derived 类型的变量,它们标识的实例仅在Base 不知道的方式上有所不同,那么d1.equals((Object)d2)` 将调用Derived 覆盖和返回false,但d1.equals(d2) 将调用Base 重载并返回true。由于 d2 在转换后等同于自身,因此与 d1 的两个比较都应该产生匹配的结果,但它们不会。
  • @supercat,对。正如我在帖子末尾提到的,将重载的辅助方法设为私有可能是个好主意,这样就不会出现此问题。
  • @aioobe:或者,我认为将equals 重写为final 可以使任何想要更新相等含义的人更新正确的方法,但如果派生类反复重写和重载,那么对equals(object) 的调用可能必须经过多个层才能到达实现它的实际方法。
【解决方案2】:

如果你有一个像你的例子一样的字段,我想

@Override public boolean equals(Object o) {
    return (o instanceof Thing) && (this.x == ((Thing) o).x);
}

是要走的路。其他任何事情都会过于复杂。但是如果你添加一个字段(并且不想通过 sun 的 80 列推荐),它看起来像

@Override public boolean equals(Object o) {
    if (!(o instanceof Thing))
        return false;
    Thing t = (Thing) o;
    return this.x == t.x && this.y == t.y;
}

我觉得比这个略丑

public boolean equals(Thing o) {
    return this.x == o.x && this.y == o.y;
}

@Override public boolean equals(Object o) {
    // note that you don't need this.equals().
    return (o instanceof Thing) && equals((Thing) o);
}

所以我的经验法则基本上是,如果需要在 override-only 中多次施放它,请执行 override-/overload-combo


次要方面是运行时开销。正如Java performance programming, Part 2: The cost of casting 解释的那样:

向下转换操作(在 Java 语言规范中也称为缩小转换)将祖先类引用转换为子类引用。这种转换操作会产生执行开销,因为 Java 要求在运行时检查转换以确保其有效。

通过使用 overload-/override-combo,编译器将在某些情况下(不是全部!)设法避免向下转换。


评论@Snehal 点,公开这两种方法可能会使客户端开发人员感到困惑:另一种选择是让重载的等于私有。保留了优雅,该方法可以在内部使用,而客户端的界面看起来像预期的那样。

【讨论】:

  • 哇,这是一个“我想听到的一切”的答案!是的,我确实考虑过让 private 重载供内部使用以避免沮丧!!
  • 谢谢。很棒的问题/答案和讨论 imo。
  • 我其实不同意。当使重载private 供内部使用时,您可以做的很少。在使public 过载时,您会遇到如下所述的问题。也许protected 是一个解决方案,因为这样您就可以在子类中重用这部分代码。
【解决方案3】:

重载等于的问题:

  • Java ie 提供的所有集合; Set、List、Map 使用重写的方法来比较两个对象。所以即使重载了equals方法,也解决不了比较两个对象的目的。另外,如果只是重载并实现 hashcode 方法,会导致错误行为

  • 如果您同时拥有重载和覆盖的 equals 方法并公开这两种方法,您将会使客户端开发人员感到困惑。按照惯例,人们认为您正在覆盖 Object 类

【讨论】:

  • OP 本人排除了“只是超载”选项。但是,您的第二点很有趣。为此+1。您如何评论将其设为私有?
【解决方案4】:

本书中有许多内容涵盖了这一点。 (它不在我面前,所以我会参考我记得的项目)

有一个完全使用equals(..) 的例子,据说不应该使用重载,如果使用 - 它应该小心使用。关于方法设计的项目警告不要使用相同数量的参数重载方法。所以 - 不,不要超载equals(..)

更新:来自“Effective Java”(第 44 页)

提供这种“强类型”equals 方法除了普通方法是可以接受的,只要这两种方法返回相同的结果,但没有令人信服的理由这样做。

所以,这样做是不被禁止的,但它会增加你的课程的复杂性,同时不会增加任何收益。

【讨论】:

  • 有一个关于明智地重载的项目,而且,如果你正在重载,请确保它们彼此之间的行为兼容(即不要让take(CharSequence)take(String)做完全不同的事情)。如果强制执行转发模式(这也是书中的建议,即在一个 contentEquals 重载到另一个之间转发),那么这里肯定存在兼容性。一旦你有机会修改它,我对这个答案的更新非常感兴趣。
  • 当然,最迟明天我会把这本书拿到我面前,我会的。 (也许删除它?:))
【解决方案5】:

我在我的项目中将这种方法与覆盖和重载组合结合使用,因为代码看起来更简洁。到目前为止,我对这种方法没有任何问题。

【讨论】:

  • 在我看来,有 2 种方法完成 1 行代码的 1 种方法的工作并不干净
【解决方案6】:

让我分享一个使用重载等于的“错误代码”示例:

class A{
    private int val;

    public A(int i){
        this.val = i;
    }

    public boolean equals(A a){
        return a.val == this.val;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(this.val);
    }
}

public class TestOverloadEquals {

    public static void main(String[] args){
        A a1 = new A(1), a2 = new A(2);
        List<A> list = new ArrayList<>();
        list.add(a1);
        list.add(a2);
        A a3 =  new A(1);

        System.out.println(list.contains(a3));
    }
}

【讨论】:

    【解决方案7】:

    我可以想到一个非常简单的例子,它不能正常工作以及为什么你不应该这样做:

    class A {
       private int x;
    
       public A(int x) {
           this.x = x;
       }
    
       public boolean equals(A other) {
           return this.x == other.x;
       }
    
       @Override
       public boolean equals(Object other) {
           return (other instanceof A) && equals((A) other);
       }
    }
    
    class B extends A{
        private int y;
    
        public B(int x, int y) {
            super(x);
            this.y = y;
        }
    
        public boolean equals(B other) {
            return this.equals((A)other) && this.y == other.y; 
        }
    
        @Override
        public boolean equals(Object other) {
            return (other instanceof B) && equals((B) other);
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            A a = new B(1,1);
            B b1 = new B(1,1);
            B b2 = new B(1,2);
    
            // This obviously returns false
            System.out.println(b1.equals(b2));
            // What should this return? true!
            System.out.println(a.equals(b2));
            // And this? Also true!
            System.out.println(b2.equals(a));
        }
    }
    

    在这个测试中,你可以清楚地看到,重载的方法在使用继承时弊大于利。在这两种错误情况下,都会调用更通用的equals(A a),因为Java 编译器只知道a 的类型是A,并且该对象没有重载的equals(B b) 方法。

    事后考虑:将重载的equals 设为私有确实可以解决这个问题,但这真的让你受益吗?它只是增加了一个额外的方法,只能通过强制转换来调用。

    【讨论】:

    • 这里的问题与重载无关。您在A 类中的equals(Object) 本身已损坏。即使您内联并删除重载,您仍然会得到 new A(1).equals(new B(1, 2)) 这是错误的,这会破坏传递性。测试getClass()read this 的相等性。
    • 即使该类测试了匹配的对象类型,所演示的问题也会存在。如果A 重载equals,那么为了正确操作B 必须覆盖equals该重载,而不仅仅是覆盖equals(object)
    【解决方案8】:

    由于这个问题已有 10 年历史,原作者可能不再对答案感兴趣,我将添加一些信息,对现在正在寻找答案的开发人员有用。

    对于 Android 开发人员,Android Studio 包含一个在尝试重载等号运算符时会弹出的模板。它还会生成一个正确的 hashCode 方法,结果被覆盖的 equals 方法如下所示:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Thing thing = (Thing) o;
        return x.equals(thing.x);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-16
      • 1970-01-01
      • 1970-01-01
      • 2011-12-30
      • 2010-11-23
      相关资源
      最近更新 更多