解读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可能相同”。