解读equals()和hashCode()

前面部分摘自:https://blog.csdn.net/javazejian/article/details/51348320 

一:Object中equals方法的实现原理

public boolean equals(Object obj) {   
    return (this == obj);     
}

每个对象都有内存地址和数据,“==”比较2个对象的地址,Object类中的equals()比较的是2个对象的地址是否相同,object1.equals(object2) 为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。

二:equals与“==”的区别

equals比较的是内容,“==”比较的是地址。

很明显,这个说法是不完全的

来看个例子

package com.zejian.test;
public class Car {
private int batch;
public Car(int batch) {
this.batch = batch;
}
public static void main(String[] args) {
Car c1 = new Car(1);
Car c2 = new Car(1);
System.out.println(c1.equals(c2));
System.out.println(c1 == c2);
}
}

console

false

false

 

分析:“==”比较的是地址,所以是false,这个不用多说.但是为什么equals也是false呢?因为所以类都集成Object类,Car类没有重写Object类里面的equals方法,所以是使用父类的equals方法,return (this == obj); 比较的亦是对象的地址,那么怎么解决这个问题呢?

那就是重写equals方法(比较内容)

@Override
public boolean equals(Object obj) {
    if (obj instanceof Car) {
        Car = (Car) obj;
        return batch == c.batch;
    }
    return false;
}        

三:equals的重写规则

自反性。对于任何非null的引用值x,x.equals(x)应返回true。

对称性。对于任何非null的引用值x与y,当且仅当:y.equals(x)返回true时,x.equals(y)才返回true。

传递性。对于任何非null的引用值x、y与z,如果y.equals(x)返回true,y.equals(z)返回true,那么x.equals(z)也应返回true。

一致性。对于任何非null的引用值x与y,假设对象上equals比较中的信息没有被修改,则多次调用x.equals(y)始终返回true或者始终返回false。

对于任何非空引用值x,x.equal(null)应返回false。

package com.zejian.test;
public class Car {
    private int batch;
    public Car(int batch) {
        this.batch = batch;
    }
    public static void main(String[] args) {
        Car c1 = new Car(1);
        Car c2 = new Car(1);
        Car c3 = new Car(1);
        System.out.println("自反性->c1.equals(c1):" + c1.equals(c1));
        System.out.println("对称性:");
        System.out.println(c1.equals(c2));
        System.out.println(c2.equals(c1));
        System.out.println("传递性:");
        System.out.println(c1.equals(c2));
        System.out.println(c2.equals(c3));
        System.out.println(c1.equals(c3));
        System.out.println("一致性:");
        for (int i = 0; i < 50; i++) {
            if (c1.equals(c2) != c1.equals(c2)) {
                System.out.println("equals方法没有遵守一致性!");
                break;
            }
        }
        System.out.println("equals方法遵守一致性!");
        System.out.println("与null比较:");
        System.out.println(c1.equals(null));
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Car) {
            Car c = (Car) obj;
            return batch == c.batch;
        }
        return false;
    }
}
自反性->c1.equals(c1):true

对称性:

true

true

传递性:

true

true

true

一致性:

equals方法遵守一致性!

与null比较:

false

子类与父类混合比较

package com.zejian.test;
public class BigCar extends Car {
    int count;
    public BigCar(int batch, int count) {
        super(batch);
        this.count = count;
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BigCar) {
            BigCar bc = (BigCar) obj;
            return super.equals(bc) && count == bc.count;
        }
        return false;
    }
    public static void main(String[] args) {
        Car c = new Car(1);
        BigCar bc = new BigCar(1, 20);
        System.out.println(c.equals(bc));
        System.out.println(bc.equals(c));
    }
}

console:

true

false

对于这样的结果,自然是我们意料之中的啦。因为BigCar类型肯定是属于Car类型,所以c.equals(bc)肯定为true,对于bc.equals(c)返回false,是因为Car类型并不一定是BigCar类型(Car类还可以有其他子类)。嗯,确实是这样。但如果有这样一个需求,只要BigCar和Car的生产批次一样,我们就认为它们两个是相当的,在这样一种需求的情况下,父类(Car)与子类(BigCar)的混合比较就不符合equals方法对称性特性了。很明显一个返回true,一个返回了false,根据对称性的特性,此时两次比较都应该返回true才对。那么该如何修改才能符合对称性呢?其实造成不符合对称性特性的原因很明显,那就是因为Car类型并不一定是BigCar类型(Car类还可以有其他子类),在这样的情况下(Car instanceof BigCar)永远返回false,因此,我们不应该直接返回false,而应该继续使用父类的equals方法进行比较才行(因为我们的需求是批次相同,两个对象就相等,父类equals方法比较的就是batch是否相同)。因此BigCar的equals方法应该做如下修改:

@Override
public boolean equals(Object obj) {
    if (obj instanceof BigCar) {
        BigCar bc = (BigCar) obj;
        return super.equals(bc) && count == bc.count;
    }
    return super.equals(obj);
}

这样运行的结果就都为true了。但是到这里问题并没有结束,虽然符合了对称性,却还没符合传递性,实例如下:

package com.zejian.test;
public class BigCar extends Car {
    int count;
    public BigCar(int batch, int count) {
        super(batch);
        this.count = count;
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BigCar) {
            BigCar bc = (BigCar) obj;
            return super.equals(bc) && count == bc.count;
        }
        return super.equals(obj);
    }
    public static void main(String[] args) {
        Car c = new Car(1);
        BigCar bc = new BigCar(1, 20);
        BigCar bc2 = new BigCar(1, 22);
        System.out.println(bc.equals(c));
        System.out.println(c.equals(bc2));
        System.out.println(bc.equals(bc2));
    }
}

console:

true

true

false

 

bc,bc2,c的批次都是相同的,按我们之前的需求应该是相等,而且也应该符合equals的传递性才对。但是事实上运行结果却不是这样,违背了传递性。出现这种情况根本原因在于:

父类与子类进行混合比较。

子类中声明了新变量,并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。

只要满足上面两个条件,equals方法的传递性便失效了。而且目前并没有直接的方法可以解决这个问题。因此我们在重写equals方法时这一点需要特别注意。虽然没有直接的解决方法,但是间接的解决方案还说有滴,那就是通过组合的方式来代替继承,还有一点要注意的是组合的方式并非真正意义上的解决问题(只是让它们间的比较都返回了false,从而不违背传递性,然而并没有实现我们上面batch相同对象就相等的需求),而是让equals方法满足各种特性的前提下,让代码看起来更加合情合理,代码如下:

package com.zejian.test;
public class Combination4BigCar {
    private Car c;
    private int count;
    public Combination4BigCar(int batch, int count) {
        c = new Car(batch);
        this.count = count;
    }
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Combination4BigCar) {
            Combination4BigCar bc = (Combination4BigCar) obj;
            return c.equals(bc.c) && count == bc.count;
        }
        return false;
    }
}

 

四:equals深入解读

前面我们一再强调了equals方法重写必须遵守的规则,接下来我们就是分析一个反面的例子,看看不遵守这些规则到底会造成什么样的后果。

package com.zejian.test;
import java.util.ArrayList;
import java.util.List;
/** * 反面例子 * @author zejian */
public class AbnormalResult {
    public static void main(String[] args) {
        List<A> list = new ArrayList<A>();
        A a = new A();
        B b = new B();
        list.add(a);
        System.out.println("list.contains(a)->" + list.contains(a));
        System.out.println("list.contains(b)->" + list.contains(b));
        list.clear();
        list.add(b);
        System.out.println("list.contains(a)->" + list.contains(a));
        System.out.println("list.contains(b)->" + list.contains(b));
    }
    static class A {
        @Override
        public boolean equals(Object obj) {
            return obj instanceof A;
        }
    }
    static class B extends A {
        @Override
        public boolean equals(Object obj) {
            return obj instanceof B;
        }
    }
}

console:

list.contains(a)->true

list.contains(b)->false

list.contains(a)->true

list.contains(b)->true

19行和24行的输出没什么好说的,将a,b分别加入list中,list中自然会含有a,b。但是为什么20行和23行结果会不一样呢?我们先来看看contains方法内部实现

@Override       
public boolean contains(Object o) { 
     return indexOf(o) != -1; 
 }

进入indexof方法

@Override
public int indexOf(Object o) {
    E[] a = this.a;
    if (o == null) {
        for (int i = 0; i < a.length; i++)
            if (a[i] == null)
                return i;
    } else {
        for (int i = 0; i < a.length; i++)
            if (o.equals(a[i]))
                return i;
    }
    return -1;
}

可以看出最终调用的是对象的equals方法,所以当调用20行代码list.contains(b)时,实际上调用了

b.equals(a[i]),a[i]是集合中的元素集合中的类型而且为A类型(只添加了a对象),虽然B继承了A,但此时

a[i] instanceof B

结果为false,equals方法也就会返回false;而当调用23行代码list.contains(a)时,实际上调用了a.equal(a[i]),其中a[i]是集合中的元素而且为B类型(只添加了b对象),由于B类型肯定是A类型(B继承了A),所以

a[i] instanceof A

结果为true,equals方法也就会返回true,这就是整个过程。但很明显结果是有问题的,因为我们的 list的泛型是A,而B又继承了A,此时无论加入了a还是b,都属于同种类型,所以无论是contains(a),还是contains(b)都应该返回true才算正常。而最终却出现上面的结果,这就是因为重写equals方法时没遵守对称性原则导致的结果,如果没遵守传递性也同样会造成上述的结果。当然这里的解决方法也比较简单,我们只要将B类的equals方法修改一下就可以了。

static class B extends A{
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof B){
            return true;
        }
        return super.equals(obj);
    }
}

到此,我们也应该明白了重写equals必须遵守几点原则的重要性了。当然这里不止是list,只要是java集合类或者java类库中的其他方法,重写equals不遵守5点原则的话,都可能出现意想不到的结果。

五.为什么重写equals()的同时还得重写hashCode()

一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

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

2.如果两个对象根据equals()方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

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

 

六、eqauls方法和hashCode方法关系

(1)同一对象上多次调用hashCode()方法,总是返回相同的整型值。

(2)如果a.equals(b),则一定有a.hashCode() 一定等于 b.hashCode()。 

(3)如果!a.equals(b),则a.hashCode() 不一定等于 b.hashCode()。此时如果a.hashCode() 总是不等于 b.hashCode(),会提高hashtables的性能。

(4)a.hashCode()==b.hashCode() 则 a.equals(b)可真可假

(5)a.hashCode()!= b.hashCode() 则 a.equals(b)为假。 

关于这两个方法的重要规范: 

规范1:若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的hashcode应该相等”。不过请注意:这个只是规范,如果你非要写一个类让equals(Object obj)返回true而hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。 

规范2:如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的hashcode可能相同”。 

 

posted @ 2019-07-25 16:28  沦为旧友  阅读(301)  评论(0编辑  收藏  举报