3

覆盖equals时总要覆盖hashCode

 3 years ago
source link: http://yuanfentiank789.github.io/2016/06/29/equals/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

覆盖equals时请遵守通用约定

不覆盖equals

不覆盖equals的情况下,类的每个实例都与它自身相等,如果满足以下任何一个条件,就是所期望的结果:

1 类的每个实例本质上都是唯一的

2 不关心类是否提供了”逻辑相等”的测试功能

3 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的(要小心)

4 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用 (不懂为什么)

PS: 逻辑相等,就是逻辑上是相等的,比如id一样,判定它们相等,即使它们是两个不同的对象

什么时候应该覆盖equals

当类需要逻辑相等这个概念的时候就应该覆盖equals 比如要判断两个student是否是同一个人,这个时候我们就需要按需重写equals

重写equals的时候就必须要遵守它的通用约定 equals方法实现了等价关系(equivalence relation):

1 自反性(reflexive) 对于任何非null的引用值x,x.equals(x)必须返回true

2 对称性(symmetric) 对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true

3 传递性(transitive) 对于任何非null的引用值,x,y,z,如果x.equals(y)为true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true

4 一致性(consistent) 对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者false

对于任何非null的引用值,x,x.equals(null)必须返回false

有些类(如集合,HashMap)与equals方法息息相关,所以重写的时候要仔细小心

高质量的equals

ej对equals提了几点建议:

1 使用==操作符检查”参数是否为这个对象的引用” 如果是,则返回true. 这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做 (平时没有用过,怎么样的比较操作算是昂贵的呢?)

2 使用instanceof操作符检查”参数是否为正确的类型” 如果不是,则返回false。

3 把参数装换成正确的类型。(这个比较好理解,instanceof检测后,一般都会强转成所需类型)

4 对于该类中的每个『关键』域,检查参数中的域是否与对象中对应的域相配。(比如学生类有学号,班级,姓名这些重要的属性,我们都需要去比对) 当你编写完成了equals方法之后,应该问自己是哪个问题:它是否是对称的、传递的、一致的?

另外EJ还告诫我们覆盖equals的时候总要覆盖hashCode(见第9条)

最后按照上诉建议,用一个Student类来总结一下equals的写法:

public class Student {
public String name;
public String className;
@Override
public boolean equals(Object obj) {
    //对于一个null的对象 我们总是返回false
    if (null == obj) {
        return false;
    }
    // 利用instanceof检查类型后,强转
    if (obj instanceof Student){
        Student other = (Student) obj;
        //再对关键的属性做比较 得出结论
        if (name.equals(other.name) && className.equals(other.className)) {
            return true;
        }
    }
    return false;
} 
}

覆盖equals时总要覆盖hashCode

覆盖了equals方法,也必须覆盖hashCode方法,if not,就违反了hashCode的通用约定,会导致无法跟基于散列的集合正常运作.

Object通用约定(在Object类中的注释即是):

在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数.在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致.

如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果.(即equals相等,那么hashCode一定相等,需要注意的是,反过来不一定成立,即hashCode相等不代表equals相等)

如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果.但是程序员应该知道,给不相等的对象产生截然不同的证书结果,有可能提高散列表(hash table)的性能.

正如之前提到的,hashCode其实主要用于跟基于散列的集合合作 如HashMap会把相同的hashCode的对象放在同一个散列桶(hash bucket)中,那么即使equals相同而hashCode不相等,那么跟HashMap一起使用,则会得到与预期不相同的结果.

具体是怎么样的不同的效果?来看一段代码: PS:Student类是第8条里的类

public static void main(String[]args) {
Student lilei = new Student("lilei","class1");
HashMap<Student, String> hashMap = new HashMap<>();
hashMap.put(lilei, lilei.className);
String className = hashMap.get(new Student("lilei","class1"));//值与之前的lilei相同,即equals会为true

System.out.println(className);
}

className的值为多少呢? class1? NO!是null!!!!(诶?)

为什么呢?因为我们并没有重写hashcode,所以即使我们去get的时候传入的Student的name以及classname与put的时候的对象值是一样的,也即它们是equals的,但是要注意,它们的hashcode是不一样的,这样就违反了上面所说的equals相等,hashCode也要相等的原则,所以当我们期望get到的是class1的时候,我们需要重写hashCode方法,让它们的hashcode相同!

那么问题来了,如何去重写hashCode呢?返回一个固定值?比如1?NO!!!

So,how?

EJ给出的解决办法:

1 把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。

2 对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤: 如果f是boolean,则计算 f?1:0
如果是byte,char,short或int,则计算 (int)f
如果是long,则计算(int)(f^(f»>32))
如果是float,则Float.floatToIntBits(s)
如果是double,则计算Double.doubleToLongBits(f),再按long类型计算一遍 如果是f是个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归调用hashCode。如果需要更复杂的比较,则为这个域计算一个‘范式’,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。 如果是个数组,则需要把每个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤b中的做法把这些散列值组合起来。 如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。 步骤(b) 按照下面公式,把(a)步骤中计算得到的散列码c合并到result中:result = 31*result+c (为什么是31呢?) 步骤(a) 为该域计算int类型的散列码c:

3 返回result

4 测试,是否符合『相等的实例是否都具有相等的散列码』

OK,知道怎么写之后,我们重写Student类的hashCode方法:

 @Override
public int hashCode() {
    int result = 17;//非0 任选
    result = 31*result + name.hashCode();
    result = 31*result + className.hashCode();
    return result;
}

这下之前的代码输出的结果为class1了!!!~

为什么要选31?

因为它是个奇素数,另外它还有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i«5)-i

终于学会如何写hashCode了! 老实说,我并没有做到这条要求! 因为一般来说我不会把Student这样的类当做一个Key去处理



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK