详解HashMap集合( 二 )

4.面试题:如果两个键的hashcode相同 , 如何存储键值对?
hashcode相同 , 通过equals比较内容是否相同 。 相同:则新的value覆盖之前的value不相同:则将新的键值对添加到哈希表中5.在不断的添加数据的过程中 , 会涉及到扩容问题 , 当超出临界值(且要存放的位置非空)时 , 扩容 。 默认的扩容方式:扩容为原来容量的2倍 , 并将原有的数据复制过来 。
6.通过上述描述 , 当位于一个链表中的元素较多 , 即hash值相等但是内容不相等的元素较多时 , 通过key值依次查找的效率较低 。 而JDK1.8中 , 哈希表存储采用数组+链表+红黑树实现 , 当链表长度(阀值)超过 8 时且当前数组的长度 > 64时 , 将链表转换为红黑树 , 这样大大减少了查找时间 。 jdk8在哈希表中引入红黑树的原因只是为了查找效率更高 。
简单的来说 , 哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的 。 如下图所示 。
但是这样的话问题来了 , 传统hashMap的缺点 , 1.8为什么引入红黑树?这样结构的话不是更麻烦了吗 , 为何阈值大于8换成红黑树?
JDK 1.8 以前 HashMap 的实现是 数组+链表 , 即使哈希函数取得再好 , 也很难达到元素百分百均匀分布 。 当 HashMap 中有大量的元素都存放到同一个桶中时 , 这个桶下有一条长长的链表 , 这个时候 HashMap 就相当于一个单链表 , 假如单链表有 n 个元素 , 遍历的时间复杂度就是 O(n) , 完全失去了它的优势 。 针对这种情况 , JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题 。当链表长度很小的时候 , 即使遍历 , 速度也非常快 , 但是当链表长度不断变长 , 肯定会对查询性能有一定的影响 , 所以才需要转成树 。
至于为什么阈值是8 , 我想 , 去源码中找寻答案应该是最可靠的途径 。下面我们在分析源码的时候会介绍 。
7.总结:
上述我们大概阐述了HashMap底层存储数据的方式 。 为了方便大家更好的理解 , 我们结合一个存储流程图来进一步说明一下:(jdk8存储过程)
说明:
1.size表示 HashMap中K-V的实时数量,注意这个不等于数组的长度。
2.threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 ) 。 这个值是当前已占用数组长度的最大值 。 size超过这个临界值就重新resize(扩容) , 扩容后的 HashMap 容量是之前容量的两倍。
3.HashMap继承关系HashMap继承关系如下图所示:
说明:

  • Cloneable 空接口 , 表示可以克隆 。创建并返回HashMap对象的一个副本 。
  • Serializable 序列化接口 。 属于标记性接口 。 HashMap对象可以被序列化和反序列化 。
  • AbstractMap 父类提供了Map实现接口 。 以最大限度地减少实现此接口所需的工作 。
补充:通过上述继承关系我们发现一个很奇怪的现象 ,就是HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口 , 那为什么HashMap还要在实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构 。
据 java 集合框架的创始人Josh Bloch描述 , 这样的写法是一个失误 。 在java集合框架中 , 类似这样的写法很多 , 最开始写java集合框架的时候 , 他认为这样写 , 在某些地方可能是有价值的 , 直到他意识到错了 。 显然的 , JDK的维护者 , 后来不认为这个小小的失误值得去修改 , 所以就这样存在下来了 。 4.HashMap集合类的成员4.1成员变量1.序列化版本号
private static final long serialVersionUID = 362498820763181265L;2.集合的初始化容量( 必须是二的n次幂 )
//默认的初始容量是16 -- 1<<4相当于1*2的4次方---1*16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;


推荐阅读