比较两个对象的相等性有哪些替代方案?

时间:2008-12-11 17:53:06

标签: java equals

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

  

事实证明,定义equals()   不是微不足道的;实际上它适度   很难做到正确,特别是在   子类的情况。最好的   问题的处理是在   霍斯特曼的核心Java第1卷。

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

编辑:

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

7 个答案:

答案 0 :(得分:4)

我不认为应始终覆盖等于是的。我理解的规则是,重写equals只有在你清楚如何定义语义等价对象的情况下才有意义。在这种情况下,您也会覆盖hashCode(),以便您没有定义为等效的对象返回不同的哈希码。

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

答案 1 :(得分:4)

  

如果必须始终覆盖equals(),   那么什么是不好的方法   被迫走投无路   对象比较?

你错了。你应该尽可能地重写equals。


所有这些信息均来自Effective Java, Second EditionJosh Bloch)。关于此的第一版章节仍然是免费的download

来自Effective Java:

  

避免问题的最简单方法是   不要覆盖equals方法,in   在哪种情况下,每个类的实例   只与自己相等。

任意覆盖equals / hashCode的问题是继承。有些等于实现主张像这样测试它:

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

实际上,Eclipse(3.4)Java编辑器在使用源工具生成方法时会执行此操作。根据布洛赫的说法,这是一个错误,因为它违反了Liskov substitution principle

来自Effective Java:

  

没有办法扩展   可实例化的类并添加一个值   组件,同时保持等于   合同。

Classes and Interfaces 一章中描述了两种最小化相等问题的方法:

  1. 赞成合成而不是继承
  2. 继承的设计和文档,或者禁止它

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

    例如,您可以定义一个记录比较方式的界面。在下面的代码中,Service实例可能在运行时被同一类的较新版本替换 - 在这种情况下,具有不同的ClassLoaders,等于比较将始终返回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);
        }
    }
    

      

    “你为什么要比较两个对象?”

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

答案 2 :(得分:3)

如何正确行事?

这是我的等于模板,它是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;
 }

答案 3 :(得分:1)

Mmhh

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

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

这种方法仅在某些情况下有用。大多数时候看起来都有点过头了。

看看this answer,了解如何实现这样的事情。

警告它是很多代码

简而言之,请参阅包装器类的工作原理since java 1.5

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

a == b 

是真的

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

是假的。

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

如您所知,Integer是只读的

与该问题所针对的String类发生了类似的事情。

答案 4 :(得分:1)

也许我错过了这一点,但是使用equals而不是定义自己的方法使用不同名称的唯一原因是因为许多集合(以及可能是JDK中的其他内容或不管它现在叫什么,都希望equals方法定义一个连贯的结果。但除此之外,我可以想到你想要做的三种比较:

  1. 这两个对象实际上是同一个实例。使用equals是没有意义的,因为你可以使用==。另外,如果我忘记了它在Java中是如何工作的,请纠正我,默认的equals方法使用自动生成的哈希码来做到这一点。
  2. 这两个对象引用了相同的实例,但它们不是同一个实例。这很有用,呃,有时候......特别是如果它们是持久化对象并引用数据库中的同一个对象。你必须定义你的equals方法才能做到这一点。
  3. 这两个对象引用了值相等的对象,但它们可能是也可能不是相同的实例(换句话说,您在层次结构中一直比较值)。
  4. 为什么要比较两个对象?好吧,如果他们是平等的,你会想做一件事,如果他们不这样做,你会想做别的事情。

    那说,这取决于手头的情况。

答案 5 :(得分:0)

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

这是至关重要的,因为我看到许多人在实践中将错误添加到集合或映射而不覆盖equals()和hashCode()。这可能是特别阴险的原因是编译器不会抱怨并且你最终会得到包含相同数据但在Collection中具有不允许重复的不同引用的多个对象。

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

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

输出:

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")));
    }
}

答案 6 :(得分:0)

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

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

有一种方法可以使等式检查(和散列)更容易出错并且更加类型安全。在Functional Java库中,您会找到Equal<A>(以及相应的Hash<A>),其中相等性被解耦为单个类。它有从现有实例为您的类编写Equal个实例的方法,以及使用{{1}的集合IterablesHashMapHashSet的包装器}和Equal<A>代替Hash<A>equals

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