java自定义equals函数和hashCode函数

 

全部类都继承自Object类,他全部的非final方法:equals,hashCode, toString, clone 和 finalize,它们都有通用约定。 咱们在覆盖这些方法的时候须要遵循这些约定,不然依赖这些约定的类(例如HashMap和HashSet)就没法结合该类一块儿工做了。java

 

一. equals

相等的概念:

  • 逻辑相等:例如Integer中包含的数值相等,咱们就认为这两个Integer相等。 再好比AbstractList中若是两个list包含的全部元素相等则两个List相等。
  • 真正意义上的相等:同一个对象。

若是不重载equals函数,那么两个类的相等只能是真正意义上的equal。若是类想要本身的相等逻辑就须要像Integer/List那样重载equals函数。sql

 

java规范中equals方法特征

  • 自反性 : 对于任何非空引用x, x.equals(x) 返回true;
  • 对称性: 对于任何引用x, y, 当且仅当y.equals(x) 返回true, x.equals(y)返回true;
  • 传递性: 对于任何引用x, y, z, 若x.equals(y)返回true, y.equals(z)返回true; 则 x.equals(z)返回true;
  • 一致性: 若x和y引用的对象没有发生改变, 则反复调用x.equals(y)应该返回一样的结果.
  • 对任意非空引用x, x.equals(null) 返回false;

下面能够经过两个不一样的状况看待这个问题:数组

  • 若是子类可以拥有本身的相等概念, 则对称性需求强制采用getClass进行检测
  • 若是由超类决定相等的概念, 那么就用instanceof进行检测,这样能够在不用子类的对象之间进行相等的比较

 

TimeStamp的不对称性ide

Date date = new Date();
Timestamp t1 = new Timestamp(date.getTime());

System.out.println("Date equals Timestamp ? : " +  date.equals(t1));// true
System.out.println("Timestamp equals Date ? : " +  t1.equals(date));// false

 

TimeStamp源码:(使用了instanceof 而不是 getClass())函数

    // Timestamp
    @Override
    public boolean equals(java.lang.Object ts) {
        if (ts instanceof Timestamp) {
            return this.equals((Timestamp)ts);
        } else {
            return false;// 非Timestamp 实例直接返回false
        }
    }
    // 省略其余代码
    public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

父类Date:性能

    // Date
    @Override
    public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }

 

备注:

  1. 在标准的java库中包含150多个equals方法的实现,包括instanceof检测, 调用getClass检测, 捕获ClassCastException检测或者什么都不作. 在java.sql.TimeStamp实现人员指出, Timestamp类继承Date类,然后者的equals方法使用了一个instanceof检测,这样重写equals方法时,就没法同时作到对称性.
  2. 在由超类决定相等时,能够考虑final关键字修改比较函数,若考虑到子类equals方法灵活性,能够不加修饰,例如AbstractSet.equals方法,应该申明为final, 这样就能够比较子类HashSet和TreeSet, 可是考虑到子类的灵活性,没有添加任何修饰.

 

编写equals方法的建议:

  1. 显示参数命名为otherObject, 稍后转化成other变量

    public boolean equals(Object otherObject)单元测试

  2. 检测this和otherObject是不是同一个对象的引用,是,返回true;

    if(this==otherObject){
    return true;
    }测试

  3. 检测otherObject是否为null, 是, 返回false;

    if(otherObject == null){
    return false;
    }优化

  4. 比较this和otherObject是否属于同一个类. 若是equals的语义在每一个子类中有所改变,就使用getClass检测:

    if(getClass() != otherObject.getClass()){
    return false;
    }this

    若是因此子类语义相同,使用instanceof检测:

    if(!(otherObject instanceof Employee)){
    return false;
    }

     

  5. 将otherObject转化为相对应的类型变量other

    Employee other = (Employee)otherObject;

  6. 对所须要的比较的数据域进行比较. 若是是基本数据类型,使用a==b比较; 若是是对象比较,调用Objects.equals(a, b)进行比较

    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);

 

2、hashCode()

设计原则中有一条: 覆盖equals时总要覆盖hashCode

hashCode编码原则:

1.只要对象equals方法的比较操做所用到的信息没有被修改,对同一对象调用屡次,hashCode方法都必须返回同一整数。在同一应用程序的屡次执行过程当中,每次执行返回的整数能够不一致。

2.若是两个对象根据equals(Object)方法比较是相等的,那么这两个对象的hashCode返回值相同。

3.若是两个对象根据equals(Object)方法比较是不等的,那么这两个对象的hashCode返回值不必定不等,可是给不一样的对象产生大相径庭的整数结果,能提升散列表的性能。


具体实例

若是一个类覆盖了equals覆盖了equals函数,却没有覆盖hashCode会违反上述第二条原则。下面看一下没有重载hashCode的例子:

public class PhoneNumber {
    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if(arg < 0 || arg > max) {
            throw new IllegalArgumentException(name + ": " + arg);

        }
    }

    @Override
    public boolean equals(Object o) {
        if(o == this)
            return true;
        if(!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

}

执行以下代码:

Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
map.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(map.get(new PhoneNumber(707, 867, 5309)));

咱们指望它返回Jenny,然而它返回的是null。

缘由在于违反了hashCode的约定,因为PhoneNumber没有覆盖hashCode方法,致使两个相等的实例拥有不相等的散列码,put方法把电话号码对象放在一个散列桶中,get方法从另一个散列桶中查找这个电话号码的全部者,显然是没法找到的

只要覆盖hashCode并遵照约定,就能修正这个问题。

 

一个好的散列函数倾向于“为不相等的对象产生不相等的散列码”,下面有简单的解决办法:

1.把某个非零的常数值,如17,保存在一个名为result的int类型的变量中。(为了2.a中计算的散列值为0的初始域会影响到散列值)

2.对于对象中的每一个关键域f,完成一下步骤:

 a.为该域计算int类型的散列码c

  i.若是该域是boolean,计算(f ? 1:0)

  ii.若是该域是byte、char、short或者int类型,则计算(int)f

  iii.若是该域是long,则计算(int)(f ^ (f >>> 32))

  iv.若是该域是float,则计算Float.floatToIntBits(f)

  v.若是该域是double,则计算Double.doubleToLongBits(f),而后

  vi.若是该域是一个对象引用,而且该类的equals方法经过递归地调用equals的方式来比较这个域,则一样为这个域递归地调用hashCode。若是须要更复杂的比较,则为这个域计算一个“范式”,而后针对这个“范式”调用hashCode。若是域的值为null,则返回0(或其余某个常数,但一般为0)。

  vii.若是该域是一个数组,则要吧每个元素当作单独的域来处理,也就是要递归地应用上述规则,对每一个重要的元素计算一个散列码,而后根据2.b把这些散列值组合起来。若是数组域中的每一个元素都很重要,可使用1.5中增长的其中一个Array.hashCode方法。

 b.按照下面的公式,把步骤2.a中计算获得的散列码c合并到result中:

  result = 31 * result + c。(选择31是由于它是一个奇素数,若是乘数是偶数,乘法溢出时会丢失信息,VM能够优化 31 * i == (i << 5) - i)

3.返回result。

编写完hashCode方法后,编写单元测试来验证相同的实例是否有相等的散列码。

把上面的解决方法应用到PhoneNumber类中:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

如今使用以前的测试代码,发现可以返回Jenny了。

相关文章
相关标签/搜索