注:博客内容主要摘抄自:重写equals时为什么也得重写hashCode之深度解读equals方法与hashCode方法渊源。
引言
同样,在翻阅《阿里巴巴Java开发手册》时,碰到一条【强制】性规则:
1) 只要重写 equals,就必须重写 hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。
这个问题博主已经碰到过多次,很经典,遂决定将其分析过程记录下来,以供以后参阅。
equals所属及内部原理
说起 equals 方法,我们都知道是超类 Object 中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在 Object 类中这个方法实际上是判断两个对象是否具有相同的引用,如果有,它们就一定相等。其源码如下:
public boolean equals(Object obj) { return (this == obj); }
实际上我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等,即若object1.equals(object2)
为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。
equals与“==”的区别
或许这是我们面试时更容易碰到的问题“equals 方法与“”运算符有什么区别?”,并且常常我们都会胸有成竹地回答:“equals 比较的是对象的内容,而“”比较的是对象的地址”。但是从前面我们可以知道 equals 方法在 Object 中的实现也是间接使用了“==”运算符进行比较的,所以从严格意义上来说,我们前面的回答并不完全正确。
因此前面的面试题我们应该这样回答更佳:默认情况下也就是从超类 Object 继承而来的 equals 方法与“==”是完全等价的,比较的都是对象的内存地址,但我们可以重写 equals 方法,使其按照我们的需求进行比较,如 String 类重写了 equals 方法,使其比较的是字符的序列,而不再是内存地址。
equals的重写规则
前面我们已经知道如何去重写 equals 方法来实现自己的需求了,但是我们在重写 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。
在通常情况下,如果只是进行同一个类两个对象的相等比较,一般都可以满足以上 5 点要求,但如果是子类与父类混合比较,那么我们就需要特别注意对称性与传递性。
接下来我们分析一个反面例子,看看不遵守这些规则到底会造成什么样的后果。
public class Car {
private int batch;
public Car(int batch) {
this.batch = batch;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Car) {
Car c = (Car) obj;
return batch == c.batch;
}
return false;
}
}
public class BigCar extends Car {
private 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 class Main {
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));
}
}
如果我们的需求是只要 BigCar 和 Car 的生产批次一样,我们就认为它们两个是相当的,我们来看一下输出结果:
true
false
可见并不符合对称性原则,在这样的情况下我们需要对 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了。但是到这里问题并没有结束,虽然符合了对称性,却还没符合传递性,如下:
public class Main {
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));
}
}
运行结果:
true
true
false
bc,bc2,c的批次都是相同的,按我们之前的需求应该是相等,而且也应该符合 equals 的传递性才对,但事实上运行结果却不是这样,违背了传递性。出现这种情况根本原因在于:
- 父类与子类进行混合比较;
- 子类中声明了新变量(count),并且在子类 equals 方法使用了新增的成员变量作为判断对象是否相等的条件。
只要满足上面两个条件,equals 方法的传递性便失效了。我们可以通过在子类中仍然只比较 batch 的值来解决问题,也可以通过组合的方式去解决这种由于使用继承而破坏 equals 重写规则的问题。具体的实现代码我不再贴出,大家以后在重写 equals 方法时只要注意这个问题就行了。
为什么重写equals的同时还得重写hashCode
这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道 Map 接口的类会使用到键对象的哈希码,当我们调用 put 方法或者 get 方法对 Map 容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在 Java 中,我们可以使用 hashCode() 来获取对象的哈希码,这个方法在 Object 类中声明,因此所有的子类都含有该方法,其值就是对象的存储地址。
在 Java API 文档中关于 hashCode 方法(这里指的是重写后的 hashCode 方法)有以下几点规定:
- 在 Java 应用程序执行期间,如果在 equals 方法比较中所用的信息没有被修改,那么在同一个对象上多次调用 hashCode 方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同;
- 如果两个对象通过调用 equals 方法是相等的,那么这两个对象调用 hashCode 方法必须返回相同的整数;
- 如果两个对象通过调用 equals 方法是不相等的,不要求这两个对象调用 hashCode 方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的 hash 值可以提高哈希表的性能。
如果只重写equals方法会产生什么后果
由于 hashCode 默认返回的是对象的存储地址,因此当我们重写 equals 方法后,判断两个对象的内容相等时,而不重写 hashCode 方法就会造成由 hashCode 方法得到的结果还是两个对象不等,这就违反了第二条规定。此时如果我们通过 Map 接口操作相关对象时,就无法达到我们预期想要的效果。具体例子如下:
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map<String,Value> map1 = new HashMap<String,Value>();
String s1 = new String("key");
String s2 = new String("key");
Value value = new Value(2);
map1.put(s1, value);
System.out.println("s1.equals(s2):" + s1.equals(s2));
System.out.println("map1.get(s1):" + map1.get(s1));
System.out.println("map1.get(s2):" + map1.get(s2));
Map<Key,Value> map2 = new HashMap<Key,Value>();
Key k1 = new Key("A");
Key k2 = new Key("A");
map2.put(k1, value);
System.out.println("k1.equals(k2):" + s1.equals(s2));
System.out.println("map2.get(k1):" + map2.get(k1));
System.out.println("map2.get(k2):" + map2.get(k2));
}
static class Key {
private String k;
public Key(String key) {
this.k = key;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Key){
Key key = (Key)obj;
return k.equals(key.k);
}
return false;
}
}
static class Value {
private int v;
public Value(int v) {
this.v = v;
}
@Override
public String toString() {
return "类Value的值-->" + v;
}
}
}
运行结果:
s1.equals(s2):true
map1.get(s1):类Value的值-->2
map1.get(s2):类Value的值-->2
k1.equals(k2):true
map2.get(k1):类Value的值-->2
map2.get(k2):null
由于 String 类重写了 equals 方法和 hashCode 方法,使其比较的是内容和获取的是内容的哈希码。因此操作 map1 所产生的结果在我们的预料之中。而由于我们在 Key 中只重写 equals 方法没有重写 hashCode 方法,从逻辑上进行判断,k1,k2 两个对象是相同的,但由于没有重写 hashCode 方法,从对象的存储地址上来看,两个对象又不等,因此造成通过 k2 所获取的值为 null。
如何重写hashCode方法
《Effective Java》中给出了一个能最大程度上避免哈希冲突的写法,但我个人认为对于一般的应用来说没有必要搞的这么麻烦。如果你的应用中 HashSet 中需要存放上万上百万个对象时,那你应该严格遵循书中给定的方法。一般来说我们不用那么麻烦,只要通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码:
public class Model {
private String name;
private double salary;
private int sex;
@Override
public int hashCode() {
return name.hashCode() + new Double(salary).hashCode()
+ new Integer(sex).hashCode();
}
}
我们将基本类型装箱为包装类型,通过包装类型得到的 hashCode 是 JDK 给我们已经重写过的,可以直接使用。当然上面仅仅是个参考例子而已,我们也可以通过其他方式去实现,只要能使散列码更加均匀。不过这里有点要注意的就是 Java 7 中对 hashCode 方法做了两个改进,首先 Java 发布者希望我们使用更加安全的调用方式来返回散列码,也就是使用 null 安全的方法Objects.hashCode()
,这个方法的优点是如果参数为 null,就只返回 0,否则返回对象参数调用的 hashCode 的结果。Objects.hashCode()
源码如下:
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
因此我们修改后的代码如下:
import java.util.Objects;
public class Model {
private String name;
private double salary;
private int sex;
@Override
public int hashCode() {
return Objects.hashCode(name) + new Double(salary).hashCode() + new Integer(sex).hashCode();
}
}
java 7 还提供了另外一个方法java.util.Objects.hash(Object... objects)
,当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码:
import java.util.Objects;
public class Model {
private String name;
private double salary;
private int sex;
@Override
public int hashCode() {
return Objects.hash(name, salary, sex);
}
}
好了,到此hashCode()
该介绍的我们都说了,还有一点要说的如果我们提供的是一个数组类型的变量的话,那么我们可以调用Arrays.hashCode()
来计算它的散列码,这个散列码是由数组元素的散列码组成的。
重写equals中getClass与instanceof的区别
虽然前面我们都在使用 instanceof,但是在重写 equals 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用 instanceof)。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下面我们来看一个例子:
public class Person {
protected String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean equals(Object object) {
if(object instanceof Person) {
Person p = (Person) object;
return name.equals(p.getName());
}
return false;
}
}
public class Employee extends Person {
private int id;
public Employee(String name,int id) {
super(name);
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object object) {
if(object instanceof Employee) {
Employee e = (Employee) object;
return super.equals(object) && e.getId() == id;
}
return false;
}
}
public class Test {
public static void main(String[] args) {
Employee e1 = new Employee("chenssy", 23);
Employee e2 = new Employee("chenssy", 24);
Person p1 = new Person("chenssy");
System.out.println(p1.equals(e1));
System.out.println(p1.equals(e2));
System.out.println(e1.equals(e2));
}
}
运行结果如下:
true
true
false
出现上面的情况就是使用了关键字 instanceof,故在覆写 equals 时推荐使用 getClass 进行类型判断。而不是使用 instanceof(除非子类拥有统一的语义)。
编写一个完美equals的几点建议
下面给出编写一个完美的 equals 方法的建议(出自 Java 核心技术 第一卷:基础知识):
- 显式参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量(参数名命名,强制转换请参考建议5);
- 检测 this 与 otherObject 是否引用同一个对象:
if(this == otherObject) return true
; - 检测 otherObject 是否为 null,如果为 null,返回 false;
- 比较 this 与 otherObject 是否属于同一个类(视需求而选择),如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:
if(getClass() != otherObject.getClass()) return false
。如果所有的子类都拥有统一的语义,就使用 instanceof 检测:if(!(otherObject instanceof ClassName)) return false
; - 将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject
; - 现在开始对所有需要比较的域进行比较。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true,否则就返回 flase。如果在子类中重新定义 equals,就要在其中包含调用
super.equals(other)
。
总结
- 熟悉 equals 方法与“==”运算符的区别;
- 熟悉 equals 方法的重写规则;
- 熟悉重写 equals 方法为何还要重写 hashCode 方法的原因;
- 熟悉重写 hashCode 方法的规则;
- 熟悉 equals 中 getClass 与 instanceof 的区别;
- 了解如何去编写一个完美的 equals 方法。