[关闭]
@mritd 2016-02-01T16:59:36.000000Z 字数 5045 阅读 1642

浅谈 equals and hashcode

java


hexo_java_equals_hashcode.jpg

equals 方法

equals 方法来源于 Object 超类;该方法用于检测一个对象与另一个对象是否相等。

Object 中的 equals

在 java 源码中,Object 的 equals 实现如下

  1. public boolean equals(Object obj) {
  2. return (this == obj);
  3. }

由此可见,Object 中 equals 默认比较的是两个对象的 内存地址(==),即 默认比较两个对象的引用,引用相同返回true,反之返回false。这看起来似乎合情合理,但实际开发中,这种比较方式则不适用;比如我们要比较两个 pserson 对象是否相等,从业务角度来说,只要这两个人 名字、年龄、身份证号相同,我们就可以认为两个对象相等。但由于是两个 pserson对象,所以所以引用肯定不同,这样调用默认的 equals 方法就会返回 false,显然是不合理的。

重写 equals

从上面的例子可以看出,Object 中的 equals 并不适用与实际业务场景,此时我们应该 对 equals进行重写;但是 重写 equals 必须满足以下规则(特性):


然而,对于以上5种特性,在某些特殊情况下需要严格考虑。

假设我们将对象内的属性看作是对象内容,在实际业务场景,可能一个 汽车 Car 对象 和一个人 pserson 对象具有相同的名字,比如 特斯拉;此时如果我们重写 equals 时仅仅比较对象内容的话,很可能误判为 一辆汽车和一个人相等;是的,这很滑稽。

getClass 的使用

在上面列举的情况来看,我们似乎再重写 equals 时还需要考虑对象的类型;在 java 里,对象类型我们 采用 Class 描述。那么此时 我们在重写的 equals 方法里应当 增加 car.getClass()==pserson.getClass() 的检测,这样能有效避免上述情况的发生;伪代码如下

  1. public boolean equals(Object obj){
  2. // 进行完全匹配检测(引用)
  3. if(this==obj) return true;
  4. // 进行空值检测
  5. if(obj==null) return false;
  6. // 进行类型匹配检测
  7. if(this.getClass()!=obj.getClass()) return false;
  8. // 进行属性相等检测,省略...
  9. }

instanceof 的使用

然而,即使我们考虑了属性相等的情况,我们还是忽略了很多其他的业务场合。比如 一个学生 Student 对象和一个人 pserson 对象;当使用上面的检测方法时,很明显 pserson 对象和 Student 对象的 Class 不一致,直接返回了 false;而实际业务场景是 一个 Student 对象也是一个人 pserson;Student 对象可能继承于pserson对象。而此时我们应当使用 instanceof 进行检测,伪代码如下:

  1. public boolean equals(Object obj){
  2. // 进行完全匹配检测(引用)
  3. if(!(this instaceof obj)) return false;
  4. // 进行空值检测
  5. if(obj==null) return false;
  6. // 进行类型匹配检测
  7. if(this.getClass()!=obj.getClass()) return false;
  8. // 进行属性相等检测,省略...
  9. }

getClass 与 instaceof 的取舍

或许从上两个例子中我们感觉使用 instaceof 更 "靠谱一些";但其实我们注意到,采用 instaceof 检测实际上违反了 对称性 原则; 因为 pserson instaceof Student 返回 false,反之返回 true。

所以对于 instanceof 有时候并不那么完美;就连 JDK的开发者也遇到了这个问题;在 Timestamp 类中,由于继承自 java.util.Date;而不幸的是 Date 类的 equals 采用的是 instanceof,这就导致对称性出了问题。从上可知,我们根据实际业务进行取舍,取舍原则如下:

重写 equals 的建议

  1. if(this==otherObject) return true;
  1. if(otherObject == null) return false;
  1. if(this.getClass()!=otherObject.getClass()) return false;

hashcode 方法

写这篇博客之前,也看过很多博客,大部分大家写的都是这样的一句话:重写 equals 必须重写 hashcode,两个对象 equals 返回 true 则 hashcode 必须保证相同。但是,接下来就没有然后了;搞的我刚学 java 时候也挺晕的,就像是 "知其然而不知所以然"。

总结一下一般会有这几个问题:

hashcode 方法是干啥的?

官方的解释是这样的:hashcode 方法用于返回一个对象的 哈希值。说白了就是 hashcode 方法能返回一个 哈希值,这玩意是个整数。

hashcode(哈希值) 是个什么玩意?

由上面可知,这个 哈希值就是一个整数,可能是正数也可能是负数。

hashcode 有什么用?

hashcode(哈希值) 的作用就是用于在使用 Hash算法实现的集合中确定元素位置。

拿我们最常见的 HashMap 来说,我们都知道 HashMap 里通过 key 取 value 时的速度 是 O(1) 级别的;

什么是 O(1)级别?

O(1)级别说白了就是 在任意数据大小的容器中,取出一个元素所使用的时间与元素个数无关;通俗的说法就是 不论你这个 HashMap 里有100个元素还是有9999999个元素,我通过 key 取出一个元素所使用的时间是一样的。

为何是 O(1) 级别?为何这么吊?

这个问题就要谈一下 HashMap 等 hash 容器的存储方式了;这些容器在存储元素是是这样的:首先获取你要存储元素的 hashcode(一个整数),然后再定义一个固定整数(标准叫桶数),最后用 hashcode 对 另一个整数(桶数) 取余;取余的结果即为元素要存储的下标(可能存放到数组里)。当然这里是简单的取余,可能更复杂。

当我们要从一个 HashMap 中取出一个 value 时,实际上他就是通过这套算法,用 key 的 hashcode 计算出元素位置,直接取出来了;所以说 无论你这里面有多少元素,它取的时候始终是用着一个算法、一个流程,不会因为你数据多少而产生影响,这就是 O(1) 级别的存储。

总结:由上面可知,这个 hashcode 的作用就是 通过算法来确立元素存放的位置,以便于放入元素或者获取元素。

我为啥要重写 hashcode && 不重写有啥后果

回顾一下上面:hashcode 是个整数,hashcode 方法的作用就是计算并返回这个整数;这个整数用于存放 Hash 算法实现的容器时 确定元素位置。

接下来考虑一个业务场景:有两个对象 pserson1 和 pserson2 ,pserson1 和 pserson2 都只有两个属性,分别是名字(name)和年龄(age)。现在 pserson1 和 pserson2 的名字(name)、年龄(age) 都相同;那么我们是否可以根据业务场景来说 pserson1 和 pserson2 是同一个人

如果说 "是" 的话,我们刚刚所认为的 "从业务角度理解 pserson1 和 pserson2 是一个人" 是不是就相当于 重写了 Pserson 的 equals 方法呢?就像下面这样:

  1. public class Pserson {
  2. private String name;
  3. private int age;
  4. // 重写 equals
  5. @Override
  6. public boolean equals(Object obj) {
  7. if (this == obj)
  8. return true;
  9. if (obj == null)
  10. return false;
  11. if (getClass() != obj.getClass())
  12. return false;
  13. // 主要在这,我们根据业务逻辑,即 姓名和年龄 确立相等关系
  14. pserson other = (pserson) obj;
  15. if (age != other.age)
  16. return false;
  17. if (name == null) {
  18. if (other.name != null)
  19. return false;
  20. } else if (!name.equals(other.name))
  21. return false;
  22. return true;
  23. }
  24. }

我们注意到,我们根据业务逻辑重写 equals 后,造成的结果就是,两个 属性相同的 Pserson 对象 我们就认为是相同的,即 equals 返回了 true;
但我们没有重写 hashcode,Object 中的 hashcode 是 native(本地的),也就是很可能不同对象返回不同的 hashcode,即使属性相同也没用。


到这里我们再总结一下:


墨迹了那么多最终问题来了:假设我们只重写了 pserson 的 equals 方法,使之 "属性相同即为相等",当我们把两个 "相等的(属性相同的)" Pserson 对象 放入 HashSet 中会怎样?

友情提示:HashSet中默认是不许放重复元素的,放重复的是会被过滤掉的,如下代码所示:

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. Pserson pserson1 = new Pserson();
  4. pserson1.setName("张三");
  5. pserson1.setAge(10);
  6. Pserson pserson2 = new Pserson();
  7. pserson2.setName("张三");
  8. pserson2.setAge(10);
  9. HashSet<Pserson> hashSet = new HashSet<Pserson>();
  10. hashSet.add(pserson1);
  11. hashSet.add(pserson2);
  12. System.out.println(hashSet.size());
  13. }
  14. }

结论&&后果:当我们仅重写了 equals 保证了 "名字和年龄一样的就是一个人" 这条业务以后;把两个 pserson 对象放入 HashSet 容器里时,由于 HashSet 是通过 hashcode 来区分两个 对象存放位置,而我们又 没有根据业务逻辑重写 hashcode 方法;导致了两个 在业务上相同的对象 放到了 HashSet里,HashSet 会认为他是两个不同的对象,故最后不会去重,hashset.size()打印出来是2。

最终结论

对于重写 euqals ,要很据实际业务逻辑来,并满足上述的设计要求;一旦重写了 equals 那就必须重写 hashcode,除非你保证你的对象不会被放到 Hash 实现的容器里;不重写的话就会导致 Hash 容器认为两个属性相同的对象是2个,而不是业务上的1个。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注