HashMap的实现原理
- 格式:docx
- 大小:47.45 KB
- 文档页数:11
HashMap中的put()和get()的实现原理
1、map.put(k,v)实现原理
(1)、⾸先将k,v封装到Node对象当中(节点)。
(2)、然后它的底层会调⽤K的hashCode()⽅法得出hash值。
(3)、通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。
如果说下标对应的位置上有链表。
此时,就会拿着k和链表上每个节点的k进⾏equal。
如果所有的equals⽅法返回都是false,那么这个新的节点将被添加到链表的末尾。
如其中有⼀个equals返回了true,那么这个节点的value将会被覆盖。
2、map.get(k)实现原理
(1)、先调⽤k的hashCode()⽅法得出哈希值,并通过哈希算法转换成数组的下标。
(2)、通过上⼀步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。
重点理解如果这个位置上什么都没有,则返回null。
如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每⼀个节点的K进⾏equals,如果所有equals⽅法都返回false,则get⽅法返回null。
如果其中⼀个节点的K和参数K进⾏equals返回true,那么此时该节点的value就是我们要找的value了,get⽅法最终返回这个要找的value。
何随机增删、查询效率都很⾼的原因是?
原因: 增删是在链表上完成的,⽽查询只需扫描部分,则效率⾼。
为什么放在hashMap集合key部分的元素需要重写equals⽅法?
因为equals⽅法默认⽐较的是两个对象的内存地址。
hashmap迭代器实现原理1.引言在计算机科学中,哈希表(H as hm ap)是一种常见的数据结构,用于存储键值对。
它通过使用哈希函数将键映射到数组的特定位置,以实现高效的查找和插入操作。
而在对哈希表进行迭代操作时,迭代器起着重要的作用。
本文将介绍ha s hm ap迭代器的实现原理。
2.哈希表和迭代器概述2.1哈希表哈希表是一种利用哈希函数来快速检索数据的数据结构。
它将键映射到数组的特定位置,并将值存储在该位置。
在哈希表中,每个位置称为一个桶(b uc ke t),可以存储一个或多个键值对。
2.2迭代器迭代器是一种提供逐个访问容器中元素的方法的对象。
通过迭代器,我们可以遍历容器中的每个元素,完成对容器的迭代操作。
3. ha shmap迭代器的实现原理3.1内部类设计在J av a中,h as hm ap的迭代器实现通常是作为h as hm ap内部类存在的。
这样设计的好处是迭代器可以访问ha s hm ap的私有成员,方便实现相关迭代操作。
3.2迭代器要素h a sh ma p迭代器的实现主要需要考虑以下几个要素:迭代器的初始化-:在迭代之前,需要对迭代器进行初始化,将其指向合适的位置。
迭代器的遍历-:根据迭代器的当前位置,依次访问哈希表中的每个键值对,完成对哈希表的遍历操作。
迭代器的操作-:迭代器通常提供一系列方法,如判断是否还有下一个元素、获取当前元素等,以支持对迭代的操作。
3.3迭代器实现步骤在实现h as hm ap迭代器时,可以按照以下步骤进行:1.定义一个内部类作为迭代器,并实现J av a标准的迭代器接口。
2.在迭代器的构造函数中,将当前位置初始化为第一个非空的桶。
3.实现迭代器接口规定的方法,如`has N ex t()`、`ne xt()`等,用于判断是否还有下一个元素,以及获取下一个元素。
4.在每个方法的实现中,注意处理边界条件和异常情况,确保迭代器的正确使用。
4.总结本文介绍了h as hm ap迭代器的实现原理。
hashmap线程安全吗首先,我们需要了解HashMap的基本原理。
HashMap是基于哈希表的实现,它通过计算键的哈希码来确定存储位置,然后将键值对存储在对应位置的链表或红黑树中。
在多线程环境下,如果有多个线程同时对HashMap进行操作,就会涉及到对同一个位置的链表或红黑树进行修改,这就可能导致数据不一致的问题。
在HashMap的早期版本中,并没有考虑多线程并发访问的情况,因此HashMap是非线程安全的。
在多线程环境下,如果没有采取额外的措施,对HashMap进行并发操作是不安全的。
然而,Java提供了一些解决方案来保证HashMap的线程安全性。
其中最常用的方法是使用ConcurrentHashMap类。
ConcurrentHashMap是Java中的线程安全的哈希表实现,它使用了锁分段技术来保证在多线程环境下的安全性。
通过将整个哈希表分割成多个小的段,每个段上都有独立的锁,不同段上的操作可以并发进行,从而提高了并发访问的性能。
除了ConcurrentHashMap之外,还可以通过使用Collections工具类的synchronizedMap方法来将HashMap包装成线程安全的Map。
这种方法虽然可以保证线程安全,但是性能通常会比ConcurrentHashMap差一些,因为它是通过在整个Map上加锁来实现线程安全的。
除了以上的方法之外,还可以通过使用读写锁来保证HashMap的线程安全性。
通过使用读写锁,可以在读操作和写操作之间进行区分,从而提高并发访问的性能。
总的来说,HashMap本身并不是线程安全的,但是可以通过使用ConcurrentHashMap、synchronizedMap或者读写锁来保证其在线程安全的情况下的安全性。
在选择具体的方法时,需要根据实际的业务场景和性能需求来进行权衡。
在使用HashMap时,需要根据实际情况来选择合适的线程安全方案,以保证程序的正确性和性能。
LinkedHashMap 是 Java 集合框架的一部分,它同时维护了插入顺序或访问顺序(取决于构造函数的参数)。
在 Android 和其他 Java 平台上,其实现原理是相同的。
以下是 LinkedHashMap 的主要实现原理:1.双向链表和哈希表的结合:LinkedHashMap 继承自 HashMap,因此它基于哈希表实现。
但是,与 HashMap 不同,LinkedHashMap 还维护了一个双向链表,用于记录插入顺序或访问顺序。
2.记录访问顺序:如果 LinkedHashMap 是按照访问顺序来排序的(构造函数中的accessOrder 参数为 true),那么每次访问一个元素(通过 get 或 put 方法),该元素都会被移动到链表的尾部。
这样,最近访问的元素总是在链表的尾部,而最久未访问的元素在链表的头部。
3.记录插入顺序:如果 LinkedHashMap 是按照插入顺序来排序的(构造函数中的accessOrder 参数为 false 或未指定),那么新插入的元素会被添加到链表的尾部。
这样,链表中的元素顺序就反映了它们的插入顺序。
4.删除最老元素:当 LinkedHashMap 达到其容量限制,并且需要添加新元素时,它会删除链表头部的元素(也就是最久未访问或最早插入的元素),以确保空间可用。
5.性能考虑:由于 LinkedHashMap 需要维护额外的链表结构,因此在插入、删除和访问元素时,它的性能略低于 HashMap。
然而,对于需要保留元素顺序的应用场景,这种性能损失通常是值得的。
在 Android 开发中,LinkedHashMap 常用于实现缓存策略,例如 LRU(最近最少使用)缓存。
通过将 accessOrder 参数设置为 true,并覆盖 removeEldestEntry 方法以在达到容量限制时删除最久未访问的元素,可以轻松地实现 LRU 缓存。
hashmap红黑树原理在Java中,HashMap是一个常用的数据结构,它的底层实现是基于哈希表和红黑树。
在插入、查找、删除方面,其时间复杂度为O(1)或者O(logn)。
那么我们就来详细了解一下HashMap红黑树的原理。
1. 哈希表HashMap的底层其实是基于哈希表的实现。
哈希表是一种通过哈希函数将键映射到位置的数据结构,可以大大加快数据的查找效率。
在Java 中,我们可以使用hashcode()函数将键转化为哈希值,然后使用哈希值与数组长度取余,确定键的位置。
2. 数组+链表当哈希冲突时(即两个键映射到了同一个位置),HashMap使用链表的方式将冲突的键值对存储在同一个位置上。
这样,当我们查找时,先通过哈希值找到对应的位置,然后遍历链表,直到找到对应的键值对。
3. 红黑树当链表长度过长时,会影响HashMap的查找效率,因此Java8中引入了红黑树来优化HashMap。
当链表长度达到阈值(默认为8)时,HashMap会将该链表转换为红黑树。
红黑树是一种高效的自平衡二叉搜索树,可以保证操作的时间复杂度为O(logn)。
4. 根据键值查找当我们使用get(key)方法来查找某个键值对时,HashMap会先根据哈希值找到对应的数组位置。
如果该位置上的元素是链表,就遍历链表,直到找到对应的键值对。
如果该位置上的元素是红黑树,就使用红黑树的查找算法在树中查找对应的键值对。
5. 插入与删除当我们使用put(key, value)方法来插入键值对时,HashMap会根据哈希值找到对应的位置。
如果该位置上的元素是空,就直接将键值对插入;如果该位置上是链表或红黑树,就判断是否有相同的键,如果有,就更新值;如果没有,就将键值对插入到链表或红黑树中。
删除操作也是类似的,就不再赘述。
综上所述,HashMap通过数组和链表/红黑树的组合,实现了高效的键值对查找效率,使得在大量数据的情况下也能够快速地实现数据的存储和查询。
hashmap的原理
Hashmap是一种常用的数据结构,它可以将键-值对映射
到一个表中,以提供快速键值查找功能。
它的原理是:将键转换为一个数字(哈希),然后这个数字就是存储在表中的位置。
Hashmap的实现需要一个哈希函数,将键转换成数字。
哈希函数有很多种,但是它们都必须有一个共同的特点:可以将任何类型的键映射到一个数字,而且这个数字是唯一的。
这样,当查找键时,只需要计算哈希函数,就可以知道键存储在哪个表位置。
Hashmap使用一个数组(桶数组)来存储键值对。
桶的大小是在建立Hashmap时预先设定的,可以根据预计的键-值对
的数量来设定桶的大小。
每个桶都有一个链表,用于存储该桶对应的键-值对。
当查找键时,只需要在桶中找到对应的键,
就能找到对应的值。
Hashmap使用一种称为“开放地址法”的冲突解决方法来处理冲突,即当哈希函数将两个键映射到同一个桶时,就会发生冲突。
当发生冲突时,会在链表中添加一个新的节点,并将其中的键值对存储在新节点中。
Hashmap提供了非常快速的查找操作,它可以在常数时间内完成查找,而不管键-值对的数量有多少。
它的缺点在于,
哈希函数的性能不好,哈希函数的计算代价可能会影响Hashmap的性能。
总的来说,Hashmap是一种非常有用的数据结构,适用于快速查找操作。
它的实现原理是将键转换为哈希,然后使用“开放地址法”来处理冲突。
它提供了非常快速的查找操作,但是哈希函数的计算效率可能会影响性能。
currenthashmap实现原理currenthashmap是Java中的一种数据结构,它是HashMap的一个变体。
在本文中,我们将探讨currenthashmap的实现原理。
让我们来了解一下HashMap的基本原理。
HashMap是一种以键值对形式存储数据的数据结构。
它使用哈希函数将键映射到存储桶中,从而实现快速的插入、查找和删除操作。
但是,在多线程环境下,HashMap可能会导致死锁和数据不一致的问题。
为了解决这个问题,Java提供了currenthashmap。
currenthashmap 采用了一种不同的并发控制策略,以确保在多线程环境下仍然能够保持高性能和数据一致性。
currenthashmap的实现原理主要包括以下几个方面:1. 分段锁:currenthashmap将整个数据结构分成了多个段(Segment),每个段都拥有自己的锁。
这样,不同的线程可以同时访问不同的段,从而提高并发性能。
2. 线程安全的操作:currenthashmap中的put和get操作都是线程安全的。
在进行put操作时,如果多个线程同时访问了同一个段,它们会根据键的哈希值决定应该放到哪个桶里。
而在进行get操作时,多个线程可以同时读取不同的段,从而提高并发性能。
3. 扩容机制:当currenthashmap的负载因子(load factor)达到一定阈值时,会触发扩容操作。
扩容时,currenthashmap会创建一个新的数据结构,并将原来的数据分配到新的结构中。
这个过程是线程安全的,不会影响其他线程对currenthashmap的访问。
4. 锁分离:currenthashmap中的锁是分段的,不同的段拥有不同的锁。
这种锁分离的设计使得不同的线程可以同时访问不同的段,从而提高并发性能。
而在普通的HashMap中,所有的操作都需要获取同一把锁,这就限制了并发性能。
5. 数据一致性:currenthashmap保证了多线程环境下的数据一致性。
Hashmap是Java中最常用的数据结构之一,它提供了一种存储key-value键值对的方式。
在Java 1.8中,HashMap的实现经过了一些改进,其中包括了扩容机制的优化。
下面我们将深入探讨HashMap 1.8版本的原理和扩容机制。
一、HashMap 1.8原理在HashMap中,键值对被存储在一个称为桶(bucket)的数组中。
这个数组的每个元素被称为一个桶,每个桶中存储着一个链表或红黑树,用来解决哈希冲突。
在HashMap 1.8中,对于链表的长度超过8的情况,会将链表转换成红黑树,以提高查找效率。
HashMap采用了数组+链表/红黑树的结构,通过计算key的哈希值,然后根据哈希值的高几位来确定存储位置。
在1.8版本中,通过引入红黑树的结构,进一步提高了对于包含大量数据的Map的性能。
二、HashMap 1.8扩容机制1. 初始容量和加载因子HashMap的初始容量和加载因子是决定其扩容的重要参数。
在1.8版本中,默认的初始容量为16,加载因子为0.75。
当HashMap中的键值对数量超过了加载因子与当前容量的乘积时,HashMap将会进行扩2. 扩容触发条件在HashMap 1.8版本中,当插入新的键值对后,如果当前的键值对数量已经超过了加载因子与当前容量的乘积,就会触发HashMap的扩容操作。
这个操作会将HashMap的容量扩大为原来的两倍,并且重新计算所有的哈希值,将键值对重新分布到新的桶中。
3. 扩容过程在扩容过程中,HashMap会创建一个新的桶数组,并且将原来的桶数组中的元素重新计算哈希值,然后重新分配到新的桶数组中。
这个过程需要一定的时间和计算资源,因此在使用HashMap时,需要根据实际情况来调整初始容量和加载因子,以减少扩容的次数和性能损耗。
4. 扩容带来的影响HashMap的扩容是为了保证HashMap在扩容后仍然能够保持较低的哈希冲突,以提高查找效率。
但是扩容也会带来一定的性能损耗,尤其是在插入大量数据时,可能会触发多次扩容操作。
java map底层实现原理JavaMap底层实现原理是什么?Map是一种数据结构,用于存储键值对映射关系。
Java中的Map接口定义了一组操作,如put(插入键值对)、get(获取键值对)、remove(移除键值对)等。
在Java中,有多种Map实现类,如HashMap、TreeMap、LinkedHashMap等。
其中,HashMap是应用最广泛的一种Map实现类。
那么,HashMap的底层实现原理是什么呢?HashMap底层使用了一个数组来存储键值对。
数组的每个元素都是一个链表,用于解决哈希冲突(即不同的键映射到相同的索引位置)。
当插入或获取一个键值对时,首先根据键的哈希码计算出该键值对应的数组索引位置,然后在该位置的链表中查找该键的节点。
因为哈希算法不是完美的,所以不同的键可能会映射到相同的数组索引位置,这就产生了哈希冲突。
为了解决哈希冲突,HashMap使用了链表法,即在同一数组索引位置上维护一个链表,每个链表节点存储一个键值对。
当发生哈希冲突时,新的键值对会被插入到该链表的尾部。
当一个键需要被查找时,HashMap首先根据该键的哈希码计算出对应的数组索引位置,然后在该位置的链表中进行线性查找,直到找到该键或者遍历完整个链表。
当HashMap的键值对数量达到一定阈值(默认为0.75倍的数组容量),就会触发扩容操作,即新建一个更大的数组,并将原有数组中的键值对重新哈希到新数组中。
总的来说,HashMap底层使用哈希表实现,通过哈希算法将键映射到数组索引位置,并使用链表法解决哈希冲突。
这种实现方式具有高效的插入、查找、删除操作,是应用最广泛的一种Map实现类。
hashmap底层原理及实现扰动算法,寻址算法
1.HashMap底层原理:
HashMap底层使用哈希表实现,它通过键的哈希值来确定位置,存取
数据使用时间复杂度为O(1),也就是说查找,插入,删除的操作的时间
复杂度大致都是一样的。
哈希表以一种数组的方式储存,它们的每个位
置称为桶,可以使用链接结构或者红黑树结构来储存,在很多情况下只会
用到链接结构。
HashMap会根据输入的键计算出哈希值,哈希值再计算出
储存的桶的索引,最后将数据存入桶里。
2.扰动算法:
HashMap底层使用扰动算法来解决哈希冲突,扰动算法是一种在冲突
发生时,增加索引位置的算法。
也就是能不能把同一个哈希值的键储存到
不同的索引位置,以避免冲突。
HashMap使用的称为线性探测的扰动算法,它会在原来的索引位置上加上一个数,并以此来确定新的索引位置,当这
个位置也有冲突时,则继续累加,直到没有冲突或者所有位置都被检查过。
3.寻址算法:
HashMap底层哈希表采用开放定址法实现,开放定址法是一种寻址算法,它使用哈希函数计算出位置,若发生冲突时,使用某种规则在原有位
置基础上,往后移动一定长度来寻找可以插入的空间,例如HashMap底层
使用的线性探测算法就是这种方法。
HashMap的实现原理1. HashMap概述:HashMap是基于哈希表的Map接口的非同步实现。
此实现提供所有可选的映射操作,并允许使用null值和null键。
此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2. HashMap的数据结构:在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。
当新建一个HashMap的时候,就会初始化一个数组。
源码如下:view sourceprint?01 /**02 * The table, resized as necessary. Length MUST Always be a power of two.03 */04 transient Entry[] table;0506 static class Entry<K,V> implements Map.Entry<K,V> {07 final K key;08 V value;09 Entry<K,V> next;10 final int hash;11 ……12 }可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。
3. HashMap的存取实现:1) 存储:view sourceprint?01 public V put(K key, V value) {02 // HashMap允许存放null键和null值。
03 // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
04 if (key == null)05 return putForNullKey(value);06 // 根据key的keyCode重新计算hash值。
07 int hash = hash(key.hashCode());08 // 搜索指定hash值在对应table中的索引。
09 int i = indexFor(hash, table.length);10 // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
11 for (Entry<K,V> e = table[i]; e != null; e = e.next) {12 Object k;13 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {14 V oldValue = e.value;15 e.value = value;16 e.recordAccess(this);17 return oldValue;18 }19 }20 // 如果i索引处的Entry为null,表明此处还没有Entry。
21 modCount++;22 // 将key、value添加到i索引处。
23 addEntry(hash, key, value, i);24 return null;25 }从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key 的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组table的i索引处。
addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:view sourceprint?01 void addEntry(int hash, K key, V value, int bucketIndex) {02 // 获取指定 bucketIndex 索引处的 Entry03 Entry<K,V> e = table[bucketIndex];04 // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry05 table[bucketIndex] = new Entry<K,V>(hash, key, value, e);06 // 如果 Map 中的 key-value 对的数量超过了极限07 if (size++ >= threshold)08 // 把 table 对象的长度扩充到原来的2倍。
09 resize(2 * table.length);10 }当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。
我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
hash(int h)方法根据key的hashCode重新计算一次散列。
此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
view sourceprint?1 static int hash(int h) {2 h ^= (h >>> 20) ^ (h >>> 12);3 return h ^ (h >>> 7) ^ (h >>> 4);4 }我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。
如何计算这个位置就是hash算法。
前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用hash(int h) 方法所计算得到的 hash 码值总是相同的。
我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。
indexFor(int h, int length) 方法的代码如下:view sourceprint?1 static int indexFor(int h, int length) {2 return h & (length-1);3 }这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方,这是HashMap在速度上的优化。
在 HashMap 构造器中有如下代码:view sourceprint?1 int capacity = 1;2 while (capacity < initialCapacity)3 capacity <<= 1;这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:h & (table.length-1) hash table.length-18 & (15-1): 0100 & 1110 = 01009 & (15-1): 0101 & 1110 = 0100---------------------------------------------------8 & (16-1): 0100 & 1111 = 0109 & (16-1): 0101 & 1111 = 0101从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。
如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。