之前再看《疯狂的Java》的时候看到在定义一个类的时候重写了HashCode和equals方法。记得之前看《Effective Java》的时候也有提起过。但时间太长已经想不起来了。今天特意上网查了一下并结合《Effective Java》写一下。
重写equals方法
上帝类Object(之前看毕老师的视频的时候经常这么叫)中有很多方法都是非final类型的。因此,这给重写提供了可能,这里面就包括equals方法。不过值得注意的是任何子类在重写的时候都要遵循一定的规则,否则会引起很多问题。
equals方法和==的区别
==方法是用于判断类型值是否相等的,比如int, float...同时也可以用来判断两个对象的引用地址是否相同(两个引用指向同一个对象)。而equals方法用于判断两个对象是否相等,需要程序员自己完成(JDK源码里的equals方法是由开发者完成的也是由程序员完成的)。这里需要注意相等不等于相同。也就是如果==返回的是true,equals返回的也一定是true。
什么时候需要重写equals方法
当需要向集合中添加元素的时候需要重写equals方法。因为添加进集合的时候首先需要判断该集合中是否含有需要添加的元素,这个时候就要使用contains方法。contains方法内部调用equals方法。
equals方法的等价关系
重写equals方法看似简单,但很多方式会导致错误,并造成严重的后果。所以Java规范对重写equals方法做出了规定。即equals方法需要实现等价关系(equivalence relation)
- 自反性(reflective):对于任何非null的引用值必须满足x.equals(x)必须为true。
- 对称性(symmetric):对于任何非null的引用值x,y。当且仅当x.equals(y)为true的时候,y.equals(x)一定为true。
- 传递性(transitive):对于任何非null的引用值x,y,z。当且仅当x.equals(y)为true的时候且y.equals(z)为true,x.equals(z)一定为true。
- 对于任何非null的引用值x,x.equals(null)一定返回false。
重写equals方法实践
首先我们有一个Employee类和Manager类
public class Employee{
String name;
Double salary;
Date date;
//getter.../setter...
}
public class Manager extends Employee{
Double bonus;
//getter.../setter...
}
如果不写equals方法
public static void main(String[] args) {
// TODO Auto-generated method stub
Employee x = new Employee();
x.setName("Jane");
x.setSalary(3500.0);
Employee y = new Employee();
y.setName("Jane");
y.setSalary(3500.0);
System.out.println(x.equals(x));
System.out.println(x.equals(y));
}
第一个返回的是true,第二个返回的是false。但是根据已知的条件第二个应该返回的是true。这说明Object类的equals方法不够用,我们需要自己定义equals方法。在自己实现equals方法的时候第一条规定自反性很容易违反。这样在我们添加元素到集合中的时候很容易将相同的元素添加进去。通常情况下我们还是实现该方式的,这也是一种性能优化的方式(如果比较的两个对象是同一对象则返回true)。
@Override
public boolean equals(Object obj) {
//使用==判断两个对象是否指向同一个对象
if(this == obj)
return true;
//对于任何非null的对象,x.equals(null)必须返回false
if(obj == null)
return false;
return false;
}
注意:@Override注解必须加上否则IDE会帮我们检查是否重写了父类方法,否则可能实现的是重载方法。导致后面运行出错找不到问题的所在。
之后实现的是对称性
@Override
public boolean equals(Object obj) {
//使用==判断两个对象是否指向同一个对象
if(this == obj)
return true;
//对于任何非null的对象,x.equals(null)必须返回false
if(obj == null)
return false;
//通过instanceof判断比较对象的类型是否合法
if(!(obj instanceof Employee)){
return false;
}
//对象强制类型转换。如果核心区域比较相等则返回true否则返回false
//强制类型转换前必须进行instanceof判断,避免代码出现ClassCastException异常
Employee other = (Employee)obj;
return ((this.name == other.name || (this.name != null && (this.name.equals(other.name))))
&&(this.salary == other.salary) || (this.salary != null && (this.salary.equals(other.salary))));
}
使用instanceof的时候需要注意,如果子类拥有统一的语义时使用instanceof检查,如果要求比较目标和当前类必须为同一类的时候需要使用
this.getClass() == obj.getClass();
使用JDK7提供的工具类优化代码
我们在写equals代码的时候经常要判断属性值是否为空。非空才比较目标对象的相同属性值是否相等。而在JDK8中提供了Objects工具类,可以帮助我们简化这部分代码。
@Override
public boolean equals(Object obj) {
// 这里使用==显示判断比较对象是否是同一对象
if (this == obj) {
return true;
}
// 对于任何非null的引用值x,x.equals(null)必须返回false
if (obj == null) {
return false;
}
// 通过 instanceof 判断比较对象类型是否合法
if (!(obj instanceof Employee)) {
return false;
}
// 对象类型强制转换,如果核心域比较相等,则返回true,否则返回false
Employee other = (Employee) obj;
// 如果两者相等,返回true(含两者皆空的情形),否则比较两者值是否相等
return Objects.equals(this.name, other.name)
&& Objects.equals(this.salary, other.salary);
}
另外该类还提供了deeEquals方法。当属性值为引用时比较使用。
重写HashCode方法
通常来说重写equals方法的时候必须重写HashCode方法。HashCode中文翻译为散列码或哈希码,由哈希算法将对象映射为一个整形数值。在Java中一般用于HashMap、HashSet、HashTable等集合类中。
为什么在重写equals方法的同时需要重写HashCode方法
我们都知道HashMap底层是由数组加链表组成的。HashCode就是数组的下标。如果不重写HashCode方法,则相同的对象会生成不同的HashCode。当插入到散列表中的时候相同的对象就会被插入。这是不符合规定的。我们知道向HashMap中插入相同值的时候会出现覆盖。
例如以下代码:
public static void main(String[] args) {
// TODO Auto-generated method stub
Employee x = new Employee();
x.setName("Jane");
x.setSalary(3500.0);
Employee y = new Employee();
y.setName("Jane");
y.setSalary(3500.0);
HashSethash = new HashSet();
hash.add(x);
hash.add(y);
System.out.println(hash.size());
}
x和y应该是相等的,插入到HashSet中的时候应该会出现覆盖,但结果确是2。
如何编写一个好的HashCode方法
相等的对象必须有相等的HashCode,但是反过来确不一定。因为存在哈希碰撞,通俗的说就是不同的对象生成的HashCode可能是相同的,而发生哈希碰撞的几率是由哈希算法决定的。一般来说发生哈希碰撞的几率越大,哈希算法也就越差。所以一个好的哈希算法是尽可能的减少哈希碰撞的几率。
在编写HashCode的时候大量使用了31这一数字。该数字有一个很好的特性用移位和减法来代替乘法。可以得到更好的性能31 * i == (i<<5) - i。
散列码的性能优化
通常不建议会被修改的属性参与HashCode的运算(实际上是难以避免的)。因为这会引起HashCode的变化。对于已经加入HashMap的对象,不会重新分配存储位置,而导致一些问题。
对于一些比较复杂的对象HashCode的计算就是一件非常消耗资源的事情。一个简单的办法就是对HashCode进行缓存,在类中添加一个HashCode属性,记录该HashCode。HashCode可以在类的初始化的时候生成也可以在第一次调用hashcode方法的时候生成。这要视具体情况而定,前提是参与计算HashCode的属性值不能更改。