1、HashMap的存储结构
数组+链表+红黑树(jdk1.8)
如下图所示:
2、HashMap的特点,如何实现
我们知道HashMap是一种可以快速存储很快速查找的键值容器,那么jdk是如何实现HashMap的快速存储和快速查找呢?
我们先从数组和链表以及二叉查找树这三种数据结构说起
1)数组
数组结构是连续的内存地址,数组的部分元素被连续存放在cpu缓存中,利用二分查找法,数组的时间复杂度位低到O(1),可见数组的查询效率是非常高的。但是由于数组的内存占用严重,空间复杂度很高,所以数组的增删操作效率将非常低下。
2) 链表
链表内存地址比较分散,空间复杂度较低,在插入和删除上效率较高。但是内存地址过于分散,导致查询效率大大降低。
3) 二叉查找树
二叉树在查询效率上和排序后数组的二分查找效率完全相同,从根节点开始,到下面分支节点左边的永远比父节点的要小,右边比父节点大。
如下图所示:
图中共12个元素,如果顺序查找,可能最多需要查找12次才能查到需要的元素,但是通过查找树后,我们查找43这个元素只需要判断4次就可以。
由此我们可以看出二叉查找树在查询效率上和排序后的数组二分查找效率是相当的。但是由于二叉树的元素过于分散,导致空间复杂度过大,在插入和删除上回非常低效。为了解决这个问题,jdk使用了红黑树这种数据结构,而红黑树在时间复杂度上可以做到 O(log n) 的高效率。
综合以上三种数据结构的特点,那么HashMap到底是如何保证高效存储和高效查询的呢?没错,HashMap有效的利用了各个数据结构的长处。
实现快速存储
快速存储是链表和红黑树以及,无移动添加数组元素的优势。
HashMap中数组的索引是通过hashCode的无符号右移16位后异或然后取余获得,公式如下
index = [(hashCode)^ (HashCode>>>16) ] / 数组的长度
通过这样的计算可以保证数组索引的分散。但是分散并不代表不会出现相同的index,也就是索引冲突(hash冲突)。在遇到索引冲突的时候,HashMap会在该索引的位置生成一个单向链表,将元素放置到next。 但是我们知道链表这种数据结构在存储方面高效,但是在查询上回非常低效。所以HashMap在链表元素大于8个的时候,会自动将链表转成红黑树,以达到查询高效,插入也高效的目的。当然,在红黑树中元素个数小于一定数量,也会变回原来的链表结构,jdk设置这个数量为6个。
这样不管是在外围 "数组" 上还是在 "链表" 上 以及 变成 "红黑树" 这种数据结构,HashMap都能做到快速存储。
HashMap对数组的扩容触发条件是数组元素达到长度的0.75 (75%),使用这样的触发条件jdk是从时间和空间角度上思考的,为了这个条件更加容易被触发,也要考虑到暂用过多内存浪费资源,75%位非常理想化的触发条件。
实现快速查找
HashMap的外围数组这点毋庸置疑,查找效率绝对不会存在问题。索引冲突变成链表,元素数量仅仅只有8个的链表,查询效率不需要考虑。 大于8个元素后变成的红黑树,二叉查找树的查询效率和数组相当,这点也不需要质疑。综合考虑在查询方面HashMap,也做到了快速查找的特性。