我们都知道,要查找一个元素,如果是顺序查找,那么时间复杂度会达到O(n) ,比如:顺序表。也有可能达到 O(logn),如:平衡树。那么要想更快,我们就可以考虑另外一种数据结构,它可以达到时间复杂度为 O(1)。如:HashMap。
**
什么是HashMap?
HashMap 是一个 key-value模型,具有映射关系,通过 key值可以找到 value值。并允许使用 null值和 null键,HashMap 不保证映射的顺序。
一.HashMap基本知识
1.哈希冲突的定义
如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址后,然后要进行插入的时候,发现该位置已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。由此可见哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀,但是我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放地址法(当发生冲突时,继续寻找下一块未被占用的存储地址,然后再放入元素即可),再散列函数法,链地址法,而 HashMap 即是采用了链地址法,也就是数组+链表的方式。
2.hashmap的实现原理
HashMap 的主干是一个 Entry 数组。Entry 是 HashMap 的基本组成单元,每一个 Entry 包含一个 key-value 键值对。
HashMap的总体结构如下:
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
3.HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值:initialCapacity默认为16,loadFactory默认为0.75
二.具体问题如下
1.HashMap的内部数据结构
数组 + 链表/红黑树
2.HashMap允许空键空值么
HashMap最多只允许一个键为Null(多条会覆盖),但允许多个值为Null
3.影响HashMap性能的重要参数
初始容量:创建哈希表时桶的数量(数组的大小),默认为 16
负载因子:哈希表在其容量扩容之前可以达到一种尺度,默认为 0.75
4.HashMap的工作原理
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象
5.HashMap中put()的工作原理
6.HashMap 的底层数组长度为何总是2的n次方
- 使数据分布均匀,减少碰撞
- 当length为2的n次方时,hash&(length - 1) 就相当于对length取模,而且在速度效率上比直接取模要快得多
具体解释:
这里我觉得可以用逆向思维来解释这个问题,我们计算桶的位置完全可以使用h % length,如果这个length是随便设定值的话当然也可以,但是如果你对它进行研究,设计一个合理的值得话,那么将对HashMap的性能发生翻天覆地的变化。
没错,JDK源码作者就发现了,那就是当length为2的N次方的时候,那么,为什么这么说呢?
第一:当length为2的N次方的时候,h & (length-1) = h % length
为什么&效率更高呢?因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高
第二:当length为2的N次方的时候,数据分布均匀,减少冲突
此时我们基于第一个原因进行分析,此时hash策略为h & (length-1)。
我们来举例当length为奇数、偶数时的情况:
length为奇数时:
length为偶数时:
从上面的图表中我们可以看到,当 length 为15时总共发生了8次碰撞,同时发现空间浪费非常大,因为在 1、3、5、7、9、11、13、15 这八处没有存放数据。
这是因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,那么最后一位为1的位置即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。
而当length为16时,length – 1 = 15, 即 1111,那么,在进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以,当 length=2^n 时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
如果上面这句话大家还看不明白的话,可以多试一些数,就可以发现规律。当length为奇数时,length-1为偶数,而偶数二进制的最后一位永远为0,那么与其进行 & 运算,得到的二进制数最后一位永远为0,那么结果一定是偶数,那么就会导致下标为奇数的桶永远不会放置数据,这就不符合我们均匀放置,减少冲突的要求了。
那么为什么默认是16呢?怎么不是4?不是8?
关于这个默认容量的选择,JDK并没有给出官方解释,那么这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。
7.JDK1.8中做了哪些优化?
- 数组+链表 改成了 数组+链表或红黑树
- 链表的插入方式从头插法改成了尾插法
- 扩容的时候1.7需要对原数组中的元素进行重新hash,定位在新数组中的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
8.HashMap线程安全方面会出现什么问题
- 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
- 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况
9.HashMap线程安全方面会出现什么问题
(1)put的时候导致的多线程数据不一致,产生错误情况
比如有两个线程 A和 B,首先 A希望插入一个 key-valul 对到HashMap 中,首先计算记录所要落到的 hash 桶的索引坐标,然后获取到该桶里面的链表头结点,而此时刚好线程 A 的时间片用完了,此时线程 B 被调度得以执行,和线程 A一样的执行过程 ,只不过线程 B 成功的将记录插到了桶里面(假设线程 A 插入的记录计算出来的 hash 桶索引和线程 B 要插入的记录计算出来的 hash 桶索引是一样的)那么当线程 B 成功插入之后, 线程 A 再次被调度运行时,它依然持有过期的链表头但是它对此一无所知(头插法),以至于它认为它应该继续采用头插法插入数据(它拿到的头节点刚好是刚刚线程B插入的节点的下一个节点),如此一来就覆盖了线程 B 插入的记录,这样线程 B 插入的记录就凭空消失了,造成了数据不一致的行为。
(2)resize引起死循环
这种情况发生在 HashMap 自动扩容时,当 2 个线程同时检测到元素个数超过 数组大小 ×负载因子 的时候。此时 2 个线程会在 put() 方法中调用resize() ,那么此时若两个线程同时修改一个链表结构会产生一个循环链表,接下来再想通过get()获取某一个元素,就会出现死循环。
10.为什么1.8改用红黑树
比如某些人恶意找到你的hash碰撞值,来让你的HashMap不断地产生碰撞,那么相同 index 位置的链表就会不断增长,当你需要对这个HashMap的相应位置进行查询的时候,就会去循环遍历这个超级大的链表,性能十分不好。java8 使用红黑树来替代超过 8个节点数 的链表后,查询方式性能得到了很好的提升,从原来的是 O(n) 到 O(logn)。
原文来自:https://potato.blog.youkuaiyun.com/article/details/106835525 感觉总结的十分好,学到了不少东西。
11.负载因子
HashMap 的默认负载因子为 0.75,如果超过 0.75,会重新 resize一个原来长度两倍的 HashMap,并且重新调用 hash方法 ,对原来的数据进行重新 hash。
负载因子=存储的元素个数 / 数组的长度
12. HashMap中的equals()和hashCode()的都有什么作用?
通过 key.hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets 的位置。如果产生碰撞,则利用 key.equals()方法去链表或树中去查找对应的节点 ,另外也是为了保持数据一致性。
13. HashMap和HashTable的区别有哪些?
Hashtable 是线程安全的,HashMap 是非线程安全的。
HashMap 可以使用 null作为 key,不过建议还是尽量避免这样使用。-HashMap 以 null作为 key时,总是存储在 table数组的第一个节点上。而Hashtable 则不允许 null作为 key
HashMap的初始容量为 16,Hashtable初始容量为 11
Hashtable 计算 hash是直接使用 key的 hashcode对 table数组的长度直接进行取模,HashMap 计算 hash对 key的 hashcode进行了二次 hash,以获得更好的散列值,然后对table数组长度取模。
14. 使用场景
非并发场景使用 HashMap,并发场景可以使用 Hashtable,但是推荐使用 ConcurrentHashMap(锁粒度更低、效率更高)。
另外在使用 HashMap时要注意 null值的判断,Hashtable 也要注意防止 put null key和 null value。