共计 5031 个字符,预计需要花费 13 分钟才能阅读完成。
Java 中 hashCode() 和 equals() 的关系是面试中的常考点,如果没有深刻思考过两者设计的初衷,这个问题将很难答复。除了应酬面试,了解二者的关系更有助于咱们写出高质量且精确的代码。
一. 根底:hashCode() 和 equals() 简介
在学习 hashCode() 和 equals() 之间的关系之前, 咱们有必要先独自地理解他俩的特点.
equals()
equals() 办法用于比拟两个对象是否相等,它与 == 相等比拟符有着实质的不同。
在万物皆对象的 Java 体系中,零碎把判断对象是否相等的势力交给程序员。具体的措施是把 equals() 办法写到 Object 类中,并让所有类继承 Object 类。这样程序员就能在自定义的类中重写 equals() 办法, 从而实现本人的比拟逻辑。
hashCode()
hashCode() 的意思是哈希值, 哈希值是经哈希函数运算后失去的后果,哈希函数可能保障雷同的输出可能失去雷同的输入(哈希值),然而不可能保障不同的输出总是能得出不同的输入。
当输出的样本量足够大时,是会产生哈希抵触的,也就是说不同的输出产生了雷同的输入。
暂且不谈抵触,就雷同的输出可能产生雷同的输入这点而言,是及其贵重的。它使得零碎只须要通过简略的运算,在工夫复杂度 O(1)的状况下就能得出数据的映射关系,依据这种个性,散列表应运而生。
一种支流的散列表实现是:用数组作为哈希函数的输入域,输出值通过哈希函数计算后失去哈希值。而后依据哈希值,在数组种找到对应的存储单元。当发生冲突时,对应的存储单元以链表的模式保留抵触的数据。
二. 漫谈:初识 hashCode() 与 equals() 之间的关系
上面咱们从一个宏观的角度探讨 hashCode() 和 equals() 之间的关系。
在大多数编程实际中,归根结底会落实到数据的存取问题上。在汇编语言时代,你须要老老实实地对每个数据操作编写存取语句。
而随着时代倒退到明天,咱们都用更不便灵便的高级语言编写代码,比方 Java。
Java 以面向对象为核心思想,封装了一系列操作数据的 api,升高了数据操作的复杂度。
但在咱们对数据进行操作之前,首先要把数据依照肯定的数据结构保留到存储单元中,否则操作数据将无从谈起。
然而不同的数据结构有各自的特点,咱们在存储数据的时候须要抉择适合的数据结构进行存储。Java 依据不同的数据结构提供了丰盛的容器类,不便程序员抉择适宜业务的容器类进行开发。
通过继承关系图咱们看到 Java 的容器类被分为 Collection 和 Map 两大类,Collection 又能够进一步分为 List 和 Set。其中 Map 和 Set 都是不容许元素反复的,严格来说 Map 存储的是键值对,它不容许反复的键值。
值得注意的是:Map 和 Set 的绝大多数实现类的底层都会用到散列表构造。
讲到这里咱们提取两个关键字 不容许反复 和散列表构造,回顾 hashCode() 和 equals() 的特点,你是否想到了些什么货色呢?
三. 解密:深刻了解 hashCode() 和 equals() 之间的关系
equals() 会有力不从心的时候
下面提到 Set 和 Map 不寄存反复的元素(key),这些容器在存储元素的时必须对元素做出判断:在以后的容器中有没有和新元素雷同的元素?
你可能会想:这容易呀,间接调用元素对象的 equals() 办法进行比拟不就行了吗?
如果容器中的存储的对象数量较少,这的确是个好主见,然而如果容器中寄存的对象达到了肯定的规模,要调用容器中所有对象的 equals() 办法和新元素进行比拟,就不是一件容易的事件了。
就算 equals() 办法的比拟逻辑简略无比,总的来说也是一个工夫复杂度为 O(n) 的操作啊。
hashCode() 小力出奇观
但在散列表的根底上,判断“新对象是否和已存在对象雷同”就容易得多了。
因为每个对象都自带有 hashCode(),这个 hashCode 将会用作散列表哈希函数的输出,hashCode 通过哈希函数计算后失去哈希值,新对象会依据哈希值,存储到相应的内存的单元。
咱们无妨假如 两个雷同的对象,hashCode() 肯定雷同,这么一来就体现出哈希函数的威力了。
因为雷同的输出肯定会产生雷同的输入,于是如果新对象,和容器中已存在的对象雷同,新对象计算出的哈希值就会和已存在的对象的哈希值产生抵触。
这时容器就能判断:这个新退出的元素曾经存在,须要另作解决:笼罩掉原来的元素(key)或舍弃。
依照这个思路,如果这个元素计算出的哈希值所对应的内存单元没有产生抵触,也就是没有反复的元素,那么它就能够直接插入。
所以当使用 hashCode() 时,判断是否有雷同元素的代价,只是一次哈希计算,工夫复杂度为 O(1),这极大地提高了数据的存储性能。
Java 设计 equals(),hashCode() 时约定的规定
后面咱们还提到:当输出样本量足够大时,不雷同的输出是会产生雷同输入的,也就是造成哈希抵触。
这么一来就麻烦了,原来咱们设定的“如果产生抵触,就意味着两个对象雷同”的规定霎时被突破,因为产生抵触的很有可能是两个不同的对象!
而令人欣慰的是咱们除了 hashCode() 办法,还有一张王牌:equals() 办法。
也就是说当两个不雷同的对象产生哈希抵触后,咱们能够用 equals() 办法进一步判断两个对象是否雷同。
这时 equals() 办法就相当重要了,这个状况下它必须要能断定这两个对象是不雷同的。
- 讲到这里就引出了 Java 程序设计中一个重要准则:
如果两个对象是相等的,它们的 equals() 办法应该要返回 true,它们的 hashCode() 须要返回雷同的后果。
但有时候面试不会问得这么间接,他会问你:两个对象的 hashCdoe() 雷同,它的 equals() 办法肯定要返回 true,对吗?
那答案必定不对。因为咱们不能保障每个程序设计者,都会遵循编码约定。
有可能两个不同对象的 hashCode()会返回雷同的后果,然而因为他们是不同的对象,他们的 equals() 办法会返回 false。
如果你了解下面的内容,这个问题就很好解答,咱们再回顾一下:
如果两个对象的 hashCode() 雷同,未来就会在散列表中产生哈希抵触,然而它们不肯定是雷同的对象呀。
当产生哈希抵触时,咱们还得通过 equals() 办法进一步判断两个对象是否雷同,equals() 办法不肯定会返回 true。
这也是为什么 Java 官网举荐咱们在一个类中,最好同时重写 hashCode() 和 equals() 办法的起因。
四. 验证:联合 HashMap 的源码和官网文档,验证两者的关系
以上的文字,是我通过思考后得出的,它有肯定根据但并非齐全牢靠。上面咱们依据 HashMap 的源码(JDK1.8)和官网文档,来验证这些推论是否正确。
通过浏览 JDK8 的官网文档,咱们发现 equals() 办法介绍的最初有这么一段话:
Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.
官网文档揭示咱们当重写 equals() 办法的时候,最好也要重写 hashCode() 办法。
也就是说如果咱们通过重写 equals() 办法判断两个对象雷同时,他们的 hash code 也应该雷同,这样能力让 hashCode()办法施展它的作用。
那它到底能发会怎么的作用呢?
咱们联合局部较为罕用的 HashMap 源码进一步剖析。(像 HashSet 底层也是通过 HashMap 实现的)
在 HashMap 中用得最多无疑是 put() 办法了,以下是 put()的源码:
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
咱们能够看到 put() 办法理论调用的是 putVal() 办法,持续跟进:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;
// 在咱们创立 HashMap 对象的时候, 内存中并没有为 HashMap 调配表的空间, 直到往 HashMap 中 put 增加元素的时候才调用 resize()办法初始化表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;// 同时确定了表的长度
//((n - 1) & hash)确定了要 put 的元素的地位, 如果要插入的中央是空的, 就能够直接插入.
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 如果产生了抵触, 就要在抵触地位的链表开端插入元素
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 要害!!! 当判断新退出的元素是否与已有的元素雷同, 首先判断的是 hash 值, 前面再调用 equals()办法. 如果 hash 值不同是间接跳过的
e = p;
else if (p instanceof TreeNode)// 如果抵触解决方案曾经变成红黑树的话, 按红黑树的策略增加结点.
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {// 解决抵触的形式仍是链表
for (int binCount = 0; ; ++binCount) {// 找到链表的开端, 插入.
if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);// 插入之后要判断链表的长度, 如果达到肯定的值就可能要转换为红黑树.
break;
}// 在遍历的过程中仍会不停地断定以后 key 是否与传入的 key 雷同, 判断的第一条件依然是 hash 值.
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;// 批改 map 的次数减少
if (++size > threshold)// 如果 hashMap 的容量达到了肯定值就要进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
咱们能够看到每当判断 key 是否雷同时,首先会判断 hash 值,如果 hash 值雷同(产生了抵触),而后会判断 key 援用所指的对象是否雷同,最终会通过 equals() 办法作最初的断定。
如果 key 的 hash 值不同,前面的判断将不会执行,间接认定两个对象不雷同。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
五. 完结
讲到这里心愿大家对 hashCode() 与 equals() 办法能有更深刻的了解,明确背地的设计思维与原理。
我之前有一个疑难,可能大家看完这篇文章后也会有:equals() 办法平时我会用到,所以我晓得它除了和 hashCode() 办法有密切联系外,还有别的用处。
然而 hashCode()呢?它除了和 equals()办法有密切联系外,还有其余用处吗?
通过在互联网上一番搜查,我目前给出的答案是没有。
也就是说 hashCode() 仅在散列表中才有用,在其它状况下没用。
当然如果这个答案不正确,或者你还有别的思考,欢送留言与我交换~
对于 hashCode() 和 equals(),你学废了么?