3

http://leepoint.net/notes-java/data/expressions/22compareobjects.html

事实证明,定义 equals() 并非易事。事实上,要做到这一点相当困难,尤其是在子类的情况下。Horstmann 的 Core Java Vol 1 对这些问题的最佳处理。

如果 equals() 必须始终被覆盖,那么什么是避免被迫进行对象比较的好方法?有哪些好的“设计”替代品?

编辑:

我不确定这是否符合我的预期。也许这个问题应该更像是“你为什么要比较两个对象?” 根据您对该问题的回答,是否有替代解决方案来进行比较?我不是说,equals 的不同实现。我的意思是,根本不使用平等。我认为关键是从这个问题开始,为什么要比较两个对象。

4

7 回答 7

4

我不认为 equals 应该总是被覆盖。据我了解,规则是覆盖 equals 仅在您清楚如何定义语义等效对象的情况下才有意义。在这种情况下,您也可以覆盖 hashCode() ,这样您就没有定义为等效返回不同哈希码的对象。

如果你不能定义有意义的等价,我看不到好处。

于 2008-12-11T18:12:39.607 回答
4

如果 equals() 必须始终被覆盖,那么什么是避免被迫进行对象比较的好方法?

你误会了。您应该尽可能少地覆盖 equals。


所有这些信息都来自Effective Java, Second Edition ( Josh Bloch )。这方面的第一版章节仍然可以免费下载

从有效的Java:

避免问题的最简单方法是不要重写 equals 方法,在这种情况下,类的每个实例都只与自身相等。

任意覆盖 equals/hashCode 的问题是继承。一些 equals 实现提倡像这样测试它:

if (this.getClass() != other.getClass()) {
    return false; //inequal
}

事实上,当您使用源工具生成方法时, Eclipse (3.4) Java 编辑器就是这样做的。根据布洛赫的说法,这是一个错误,因为它违反了里氏替换原则

从有效的Java:

没有办法在保留 equals 契约的同时扩展一个可实例化的类并添加一个值组件。

类和接口一章描述了两种最小化相等问题的方法:

  1. 优先组合而不是继承
  2. 设计和文件继承或禁止它

据我所知,唯一的选择是以类外部的形式测试相等性,以及如何执行将取决于类型的设计和您尝试使用它的上下文。

例如,您可以定义一个接口来记录如何进行比较。在下面的代码中,Service 实例可能在运行时被同一类的较新版本替换——在这种情况下,具有不同的 ClassLoader,equals 比较将始终返回 false,因此覆盖 equals/hashCode 将是多余的。

public class Services {

    private static Map<String, Service> SERVICES = new HashMap<String, Service>();

    static interface Service {
        /** Services with the same name are considered equivalent */
        public String getName();
    }

    public static synchronized void installService(Service service) {
        SERVICES.put(service.getName(), service);
    }

    public static synchronized Service lookup(String name) {
        return SERVICES.get(name);
    }
}

“你为什么要比较两个物体?”

一个明显的例子是测试两个 Strings 是否相同(或两个FilesURIs)。例如,如果您想构建一组文件来解析怎么办。根据定义,该集合仅包含唯一元素。Java 的Set类型依赖 equals/hashCode 方法来强制其元素的唯一性。

于 2008-12-11T19:28:20.140 回答
3

做对了怎么样?

这是我的 equals 模板,它是 Josh Bloch 从 Effective Java 中应用的知识。阅读本书了解更多详情:

@Override
public boolean equals(Object obj) {
    if(this == obj) {
        return true;
    }

    // only do this if you are a subclass and care about equals of parent
    if(!super.equals(obj)) {
        return false;
    }
    if(obj == null || getClass() != obj.getClass()) {
        return false;
    }
    final YourTypeHere other = (YourTypeHere) obj;
    if(!instanceMember1.equals(other.instanceMember1)) {
       return false;
     }
     ... rest of instanceMembers in same pattern as above....
     return true;
 }
于 2008-12-11T18:06:27.060 回答
1

嗯嗯

在某些情况下,您可以使对象不可修改(只读)并从单点创建它(工厂方法)

如果需要两个具有相同输入数据(创建参数)的对象,工厂将返回相同的实例引用,然后使用“==”就足够了。

这种方法仅在某些情况下有用。而且大多数时候看起来有点矫枉过正。

看看这个答案,了解如何实现这样的事情。

警告这是很多代码

简而言之,看看包装类是如何从 java 1.5 开始工作的

Integer a = Integer.valueOf( 2 );
Integer b = Integer.valueOf( 2 );

a == b 

是真的,而

new Integer( 2 ) == new Integer( 2 )  

是假的。

如果输入值相同,它会在内部保留引用并返回它。

如您所知,整数是只读的

与该问题有关的 String 类也发生了类似的情况。

于 2008-12-11T18:04:21.293 回答
1

也许我错过了这一点,但使用 equals 而不是用不同的名称定义自己的方法的唯一原因是因为许多集合(可能还有 JDK 中的其他东西或现在所谓的任何东西)都期望使用 equals 方法定义一个连贯的结果。但除此之外,我可以想到您想要在 equals 中进行的三种比较:

  1. 这两个对象实际上是同一个实例。使用 equals 是没有意义的,因为你可以使用 ==。另外,如果我忘记了它在 Java 中是如何工作的,请纠正我,默认的 equals 方法使用自动生成的哈希码来执行此操作。
  2. 这两个对象具有对相同实例的引用,但不是同一个实例。这很有用,呃,有时......特别是如果它们是持久对象并引用数据库中的同一个对象。你必须定义你的 equals 方法来做到这一点。
  3. 这两个对象具有对值相等的对象的引用,尽管它们可能是也可能不是相同的实例(换句话说,您在整个层次结构中一直比较值)。

为什么要比较两个对象?好吧,如果他们是平等的,你会想做一件事,如果他们不是,你会想做别的事。

也就是说,这取决于手头的情况。

于 2008-12-11T19:19:20.580 回答
0

在大多数情况下覆盖 equals() 的主要原因是检查某些集合中的重复项。例如,如果您想使用 Set 来包含您创建的对象,则需要在您的对象中覆盖 equals() 和 hashCode()。如果您想将自定义对象用作 Map 中的键,这同样适用。

这一点很重要,因为我看到很多人在实践中犯了一个错误,即在没有覆盖 equals() 和 hashCode() 的情况下将自定义对象添加到 Sets 或 Maps。这可能特别阴险的原因是编译器不会抱怨,您最终可能会得到多个对象,这些对象包含相同的数据,但在不允许重复的 Collection 中具有不同的引用。

例如,如果您有一个名为 NameBean 的简单 bean,它有一个字符串属性“name”,您可以构造两个 NameBean 实例(例如 name1 和 name2),每个实例都具有相同的“name”属性值(例如“Alice”)。然后,您可以将 name1 和 name2 添加到 Set 中,并且该集合的大小为 2,而不是预期的大小 1。同样,如果您有一个 Map (例如 Map)以便将名称 bean 映射到其他对象,并且您首先将 name1 映射到字符串“first”,然后将 name2 映射到字符串“second”,那么您将拥有两个键/值对在地图中(例如 name1->"first", name2->"second")。因此,当您进行地图查找时,它将返回映射到您传入的确切引用的值,即 name1、name2 或另一个名为“的引用”

这是一个具体示例,前面是运行它的输出:

输出:

Adding duplicates to a map (bad):
Result of map.get(bean1):first
Result of map.get(bean2):second
Result of map.get(new NameBean("Alice"): null

Adding duplicates to a map (good):
Result of map.get(bean1):second
Result of map.get(bean2):second
Result of map.get(new ImprovedNameBean("Alice"): second

代码:

// This bean cannot safely be used as a key in a Map
public class NameBean {
    private String name;
    public NameBean() {
    }
    public NameBean(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return name;
    }
}

// This bean can safely be used as a key in a Map
public class ImprovedNameBean extends NameBean {
    public ImprovedNameBean(String name) {
        super(name);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if(obj == null || getClass() != obj.getClass()) {
            return false;
        }
        return this.getName().equals(((ImprovedNameBean)obj).getName());
    }
    @Override
    public int hashCode() {
        return getName().hashCode();
    }
}

public class MapDuplicateTest {
    public static void main(String[] args) {
        MapDuplicateTest test = new MapDuplicateTest();
        System.out.println("Adding duplicates to a map (bad):");
        test.withDuplicates();
        System.out.println("\nAdding duplicates to a map (good):");
        test.withoutDuplicates();
    }
    public void withDuplicates() {
        NameBean bean1 = new NameBean("Alice");
        NameBean bean2 = new NameBean("Alice");

        java.util.Map<NameBean, String> map
                = new java.util.HashMap<NameBean, String>();
        map.put(bean1, "first");
        map.put(bean2, "second");
        System.out.println("Result of map.get(bean1):"+map.get(bean1));
        System.out.println("Result of map.get(bean2):"+map.get(bean2));
        System.out.println("Result of map.get(new NameBean(\"Alice\"): "
                + map.get(new NameBean("Alice")));
    }
    public void withoutDuplicates() {
        ImprovedNameBean bean1 = new ImprovedNameBean("Alice");
        ImprovedNameBean bean2 = new ImprovedNameBean("Alice");

        java.util.Map<ImprovedNameBean, String> map
                = new java.util.HashMap<ImprovedNameBean, String>();
        map.put(bean1, "first");
        map.put(bean2, "second");
        System.out.println("Result of map.get(bean1):"+map.get(bean1));
        System.out.println("Result of map.get(bean2):"+map.get(bean2));
        System.out.println("Result of map.get(new ImprovedNameBean(\"Alice\"): "
                + map.get(new ImprovedNameBean("Alice")));
    }
}
于 2008-12-11T22:13:37.063 回答
0

平等是逻辑的基础(参见恒等律),没有它就没有多少编程可以做。至于比较您编写的类的实例,这取决于您。如果您需要能够在集合中找到它们或将它们用作 Maps 中的键,则需要进行相等性检查。

如果您已经用 Java 编写了多个重要的库,您就会知道很难做到等式,尤其是当胸部唯一的工具是equalshashCode. 平等最终与类层次结构紧密耦合,这使得代码变得脆弱。更重要的是,没有提供类型检查,因为这些方法只接受 Object 类型的参数。

有一种方法可以使相等检查(和散列)更不容易出错并且更安全。在Functional Java库中,您会发现Equal<A>(和相应的Hash<A>)等式被解耦为单个类。它具有Equal从现有实例为您的类组合实例的方法,以及使用and代替and的 Collections、 IterablesHashMapHashSet的包装器。Equal<A>Hash<A>equalshashCode

这种方法的最佳之处在于,您永远不会忘记在调用 equals 和 hash 方法时编写它们。类型系统将帮助您记住。

于 2008-12-13T08:04:06.737 回答