程序员人生 网站导航

对象相等性——如何给自定义对象添加equals和hashCode方法

栏目:综合技术时间:2016-07-11 09:08:04

译自 http://www.javaworld.com/article/2072762/java-app-dev/object-equality.html

每一个Java对象都从java.lang.Object继承了1些方法:

Creational methods
Object()Default no-argument constructor
clone()Returns a new instance of the class
**Equality methods**
equals(Object)Returns true if this instance is equals to the argument
hashCode()Returns a hash code based on the instance data
**Synchronizing methods**
notify()Sends a signal to a waiting thread (on the current instance)
notifyAll()Sends a signal to all waiting threads (on the current instance)
wait()Forces the current thread to wait for a signal (on the current instance)
**Other methods**
toString()Returns a string representation of the object
finalize()Perform garbage-collection duties
getClass()Returns the Class object associated with the instance

这些方法都提供了默许实现,其中除notify(), notifyAll(), wait()3个方法是final的,没法被子类重写,其他方法都可以被重写。这篇文章将讨论如何重写equals()hashCode()方法。

equals()和hashCode()方法做甚么?

equals()方法的目的是判断参数对象和当前实例是不是相等。实际上,java.util包中的所有集合类都使用了该方法,还有其他很多较为底层的库(如RMI,JDBC,等等)都隐式地依赖于该方法的正确性。如果两个对象被认为是相等的,那末该方法返回tree,否则返回false。哪些内容相等才被认为是两个对象相等由每一个类自己定义。

由于计算对象是不是相等是1件很耗时的事,Java提供了1种快速判断两个对象是不是相等的方法,即hashCode()。该方法根据对象的内部数据结构生成1个小的数值,被称为哈希码(hash code),如果两个对象具有不同的哈希码,那末他们不可能相等。(比如字典里的两个英文单词,如果它们都以A开头,那末它们有可能相等,如果1个以A开头,1个以B开头,那末它们不可能相等。)

计算哈希码的目的是哈希要比计算全部对象的相等性快。HashMap就使用了哈希码来尽量地避免计算对象的相等性,HashMapList快的1个缘由就是,List需要搜索全部数据结构判断对象是不是存在,而HashMap只需搜索那些具有相同哈希值的对象。

切记,1个类只重写equals()方法而不重写hashCode()方法是毛病的。在继承体系中,只需父类提供1个hashCode()方法便可,后面会详细讨论。

实现equals()方法

方法签名必须为

public boolean equals(Object obj)
`</pre>

注意:任何类的`equals()`方法的参数都必须是`Object`类型,否则该方法就不是重写,而是重载了,当判断两个对象是不是相等时就会调用`java.lang.Object`类的默许的`equals()`方法,而非你定义的。

Javadoc中描写`equals()`方法必须满足:
  • 自反性(Reflexive),1个对象必须和本身相等,即`a.equals(a);
  • 对称性(Symmetric),即如果a.equals(b),那末b.equals(a)
  • 传递性(Transitive),即如果a.equals(b),并且b.equals(c),那末a.equals(c)
  • 非null,1个对象任什么时候候都不能等于null,即a.equals(null)永久返回false。

    根据上面规则,很容易写出1个equals()方法的实现,只需要比较以下内容:

    1. 如果参数是this,返回true;(自反性)
    2. 如果参数是null,返回false;(非null)
    3. 如果参数类型和当前对象类型不同,返回false;(对称性)
    4. 所有非static和非transient域都是相等的。(对称性,传递性)

    为什么不需要比较static域和transient域?由于static数据是属于类的而非对象实例,所有对象实例同享static数据。transient关键字的目的是对象在序列化时不让某些域写入(如为安全起见,用户的银行卡号和密码等信息,不希望在网络操作中被传输,那末对这些变量加上transient关键字后就不会被持久化 [详见]),如果这些域用于测试对象是不是相等,那末同1个对象在序列化之前和以后就会不等。

    以2维平面坐标中的点Point为例,我们实现1个简单的equals()方法:

    `public class Point {
        private static double version = 1.0;
        private transient double distance;
        private int x, y;
    
    
    public boolean equals(Object other) {
        if (other == this) return true;
        if (other == null) return false;
        if (getClass() != other.getClass()) return false;
        Point point = (Point)other;
        return (x == point.x &amp;&amp; y = point.y);
    }
    

    }
    `

    注意:这里使用getClass()来比较两个对象是不是属于同1个类型,而非instancof,后面我们会讨论为什么不用instanceof

    比较援用类型

    如果1个对象里含有援用类型,那末如何比较两个对象的援用是不是相等?答案是根据以下规则:

    1. 如果this的援用变量为null,那末other相应的援用变量也必须为null
    2. 如果this的援用变量不为null,那末它必须和other相应的援用变量equals()

    以下面的Person类,含有两个援用类型的变量name和birth:

    `public class Person {
        private String name;
        private Date birth;
    
    
    public boolean equals(Object other) {
        if (other == this) return true;
        if (other == null) return false;
        if (getClass() != other.getClass()) return false;
        Person person = (Person)other;
        return (name == person.name || (name != null &amp;&amp; name.equals(person.name)))
        &amp;&amp; (birth == person.birth || (birth != null &amp;&amp; birth.equals(person.birth)));
    }
    

    }
    `

    注意:name == person.name检查了两个援用都为null的情况和两个援用指向同1个对象的情况。在调用name.equals(person.name)方法前1定要先检查name != null,否则将会抛出’NullPointerException’。

    实现hashCode()方法

    哈希码(hash code)就是根据实例数据计算出来的1个int值,如果两个实例被认为是equals,那末它们必须具有相同的hash code。因此,hash code**只能**根据equals()方法中所比较的变量域来计算,但其实不1定需要使用所有的域。

    2维平面上的点PointhashCode()实现以下:

    `public class Point {
        private int x, y;
    
    
    public boolean equals(Object other) {
        // see above
    }
    
    publice int hashCode() {
        return x;
    }
    

    }
    `

    上面的hashCode()实现时正确的(虽然不是最优的),由于它依赖于equals()方法的比较域x

    固然,我们期望不同对象的hash code越分散越好。上面的实现中,所有具有相同x坐标的点都具有相同的hash code,我们可以通过以下方法改进:

  • 使用多个变量的乘积;

  • 使用位运算异或^
  • 给整数变量乘上1个质数。

    `public class Point { 
    private int x, y;

    public boolean equals(Object other) {
        // see above
    }
    
    publice int hashCode() {
        return 31*x ^ 37*y;
    }
    

    }
    `

    但是,为了使hash code的计算速度快,1般不使用乘法。

    带援用类型的hashCode()方法

    如果1个类包括了援用变量,那末就能够使用援用变量的hashCode()方法。但是,跟equals()方法1样,1定要注意援用是不是为null,否则可能抛出NullPointerException异常。对null的情况,可以返回1个固定值(这个固定值应当是正整数,由于在1些hash map的实现中这些值可能有特殊的意义)。

    `public class Person {
        private String name;
        private Date birht;
    
    
    public boolean equals(Object other) {
        // see above
    }
    
    public int hashCode() {
        return (name == null ? 17 : name.hashCode()) 
            ^ (birht == null ? 31 : birth.hashCode());
    }
    

    }
    `

    equals()和hashCode()方法的默许实现

    这两个方法的默许实现只适用于简单的情况。equals方法只有在与其本身比较时才返回truehashCode()方法依赖于单1对象的哈希(unique instance hash,如对象在内存中的地址或不同虚拟机有不同的实现)。

    由于hashCode()依赖于对象的唯1性(identity),所以只重写equals()方法而不重写hashCode()方法是毛病的。否则,两个对象多是相等的,却有不同的哈希值。如果实在没有适合的哈希值计算方法,那末可以返回1个常数(比如7),这样也比使用默许的实现好,虽然这样会致使Map退化成List

    高级策略

    equals()hashCode()必须都尽量地快,由于它们常常被重复地调用。

    equals()方法的某些部份要比其他部份快,所以可以先比较快的部份。比如,基本数据类型的比较要快于援用类型的equals(),那末可以先比较基本数据类型。一样地,如果两个对象类型不同,那末就没必要比较任何数据域了。

    只读对象

    如果1个对象是只读的(immutable),那末可以提早计算出它的哈希值。当这样的对象被创建时,所有的值都会通过构造函数传入,这时候就能够计算出它的哈希值。

    `public class Point {
        private final int x, y;
        private final int hashCode;
    
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
        this.hashCode = 31*x ^ 37*y;
    }
    
    public boolean equals(Object other) {
        // see above
    }
    
    public int hashCode() {
        return hashCode;
    }
    

    }
    `

    注意,上面代码中的变量x和y都是final的,保证以后不会被改变,所以可以在创建对象时就计算出哈希值。

    为什么不用instanceof

    在Joshua Bloch非常着名的《Effective Java》1书中,他推荐使用instanceof来测试以决定对象的类型,表面上看这是1个很好的想法,实际上这有1个重大缺点,由于instanceof不是对称的。

    下面是Bloch推荐的做法:

    `public class BadPoint {
        private int x, y;
    
    
    public boolean equals(Object other) {
        if (other == this) return true;
        if (!(other instanceof BadPoint)) return false; // Bad!!!
        BadPoint point = (BadPoint)other;
        return (x == point.x &amp;&amp; y == point.y);
    }
    
    public int hashCode() {
        return x + y;
    }
    

    }
    `

    由于这个代码更短,而且是《Effective Java》1书所推荐,所以已根深蒂固于很多Java程序员的编码中。这同样成为本书最具争议的内容之1。

    使用instanceof的最大问题是它不具有对称性,当使用继承时这个问题将会体现出来:

    `public class BadPoint3D extends BadPoint {
        private int z;
    
    
    public boolean equals(Object other) {
        if (!super.equals(other)) return false;
        if (!(other instanceof BadPoint3D)) return false; // Bad!!!
        BadPoint3D point = (BadPoint3D)other;
        return (z == point.z);
    }
    

    }
    `

    当1个BadPoint对象和1个BadPoint3D对象比较时就会出现问题,badPoint instanceof badPoint3D == false,而point3D instanceof badPoint == true

    完全的写法

    Point

    `public class Point {
        private static double version = 1.0;
        private transient double distance;
    
    
    private String name;
    private int x, y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public Point(String name, int x, int y) {
        this(x, y);
        this.name = name;
    }
    
    public boolean equals(Object other) {
        if (other == this) return true;
        if (other == null) return false;
        if (getClass() != other.getClass()) return false;
        Point point = (Point)other;
        return (x == point.x &amp;&amp; y == point.y 
            &amp;&amp; (name = point.name || (name != null &amp;&amp; name.equals(point.name))));
    }
    
    public int hashCode() {
        return x ^ y;
    }
    

    }
    `

    Point3D

    `public class Point3D {
        private int z;
    
    
    public Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }
    
    public Point3D(String name, int x, int y, int z) {
        super(name, x, y);
        this.z = z;
    }
    
    public boolean equals(Object other) {
        if (!super.equals(other)) return false;
        Point3D point = (Point3D)other;
        return (z == point.z);
    }
    
    public int hashCode() {
        return super.hashCode() ^ z;
    }
    

    }

更深的学习可以参看 如何在Java中避免equals方法的隐藏圈套 | 酷 壳 - CoolShell.cn

说明:本文译自javaword.com,如果侵权,请联系站长删除。同时,如果转载本文,请注明本文链接。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐