HashMap是Map接口的实现类,以key-value存储形式存储数据。HashMap的操作不是同步的,所以线程不安全。
特点:
无序性 : 存入取出元素顺序不一致
唯一性 : key唯一
可存null : 键和值都可以为null,键只能有一个为null
数据结构 : 数据结构控制的是key而非值value
HashMap类的继承关系
说明:
Cloneable 空接口,表示可以克隆。 创建并返回HashMap对象的一个副本。
Serializable 序列化接口。属于标记性接口。HashMap对象可以被序列化和反序列化。
AbstractMap 父类提供了Map实现接口。以最大限度地减少实现此接口所需的工作。
补充:HashMap已经继承了AbstractMap而 AbstractMap类实现了Map接口,为什么HashMap还要在实现Map接口呢?同样在ArrayList中 LinkedList中都是这种结构。
据java集合框架的创始人Josh Bloch描述,这样的写法是一个失误。最开始他认为这样写在某些地方可能是有价值的。在java集合框架中,类似这样的写法很多。
JDK的维护者不认为这个小小的失误值得去修改,所以就这样存在下来了。
HashMap原理分析
什么是哈希表?
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。
它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希表本质上是一个数组,这个数组中存储的是哈希函数算出的值。
目的 : 为了加快数据查找的速度。
HashMap存储数据过程
加载因子 : 默认值是0.75 ,决定了扩容的条件
// 加载因子 final float loadFactor;
扩容的临界值 : 计算方式为(容量 乘以 加载因子)
// 临界值 当实际大小超过临界值时,会进行扩容 int threshold;
容量capacity : 初始化为16
扩容resize : 达到临界值就扩容。扩容后的 HashMap 容量是之前容量的两倍 。
集合元素个数size : 表示HashMap中键值对实时数量,不等于数组长度。
jdk8存储过程
存储过程源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //1.判断是否哈希表为空 if ((tab = table) == null || (n = tab.length) == 0) //2.如果为空初始化容量,16 n = (tab = resize()).length; //3.如果不为空 , 则判断当前key的hash值对应的索引位置是否有元素。 if ((p = tab[i = (n - 1) & hash]) == null) //4.如果没有,往当前索引位置放入一个新的节点 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //5.如果有元素,判断当前索引位的节点hash值和equals与新key是否相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //如果相等,则覆盖value e = p; //6.如果不相等,则判断是否是红黑树 else if (p instanceof TreeNode) //如果是红黑树节点,则将元素存入红黑树节点 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //7.如果不相等,也不是红黑树节点,则遍历所有链表节点 for (int binCount = 0; ; ++binCount) { //如果到了后一个节点还没找到相等的节点 if ((e = p.next) == null) { //在尾部新增一个节点 p.next = newNode(hash, key, value, null); //8.判断链表的长度是否大于8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果大于8直接将链表转换为红黑树 treeifyBin(tab, hash); break; } //如果遍历的节点的hash值和equals值与新key相同,则跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果key存在,则直接覆盖value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //判断HashMap中节点数是否大于临界值,如果大于则扩容,是之前的两倍 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap底层数据结构
jdk1.8之前数据结构是:链表 + 数组
jdk1.8之后数据结构是:链表 + 数组 + 红黑树 。单链表阈值(边界值) > 8 且数组长度大于64,才将链表转换为红黑树。 目的 : 高效查询数据
扩展知识: 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数 据结构,典型的用途是实现关联数组。红黑树是在1972年由Rudolf Bayer发明的,当时被称为平 衡二叉B树(symmetric binary B-trees)
数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
什么是哈希冲突?两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同。
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其 实就是根据对象的hashCode和equals方法来决定的。
如果我们往集合中存放自定义的对象,那么保证 其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。 当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效 率较低。
而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组 的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只 是为了查找效率更高。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示。
HashMap中哈希表的数组的大小?
创建HashMap集合对象时
JDK8前,构造方法创建一个长度是16的数组Entry[] table 来存储键值对的对象。
JDK8后,不是在构造方法中创建对象数组,而是在第一调用put方法时创建长度是16的Node[] table数组,存储Node对象
如果节点长度即链表长度大于阈值8,并且数组长度大于64则进行将链表变为红黑树。
数据结构的源码
table用来初始化(必须是二的n次幂)(重点)
//存储元素的数组 transient Node<K,V>[] table;
用来存缓存
//存放具体元素的集合 transient Set<Map.Entry<K,V>> entrySet;
HashMap中存放元素的个数(重点)
//存放元素的个数,注意这个不等于数组的长度。 transient int size;
HashMap源码分析
初始化容量16
//默认的初始容量是16 -- 1<<4相当于1*2的4次幂---1*16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
初始化容量必须是2的n次幂,为什么?
向HashMap中添加元素时,要根据key的hash值去确定其在数组中的具体位置。
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同。
怎么让元素均匀分配呢?
这里用到的算法是hash&(length-1)。hash值与数组长度减一的位运算。算法本质作用是类似于取模, hash%length。
但是计算机中直接求余效率远不如位运算。 hash%length取模效果操作等于hash&(length-1)的前提是length是2的n次幂!
如果不考虑效率问题,求余即可。就不需要长度必须是2的n次幂了。如果采用位运算,必须 是2的n次幂!
为什么这样能均匀分布减少碰撞呢?
2的n次幂实际就是1后面n个0,2的n次幂-1 实际就是n个1;
举例:位运算规则说明:按&位运算(相同位的两个数字都为1,则为1;若有一个不为1,则为0)。
例如 : 数组长度8时候,均匀分布在数组中,哈希碰撞的几率比较小; 求位运算结果: 314924944 & (8-1) = 0 00010010110001010101111110010000 00000000000000000000000000000111 -------------------------------------------------- 00000000000000000000000000000000 --> 结果为0 程序员计算器求解 : 314924944 & (8-1) = 0 314924945 & (8-1) = 1 314924946 & (8-1) = 2 314924947 & (8-1) = 3 314924948 & (8-1) = 4 314924949 & (8-1) = 6 314924950 & (8-1) = 7 314924951 & (8-1) = 8 314924952 & (8-1) = 0 结论是:数组索引存储的数据均匀分布了,减少哈希碰撞的几率 例如 : 数组长度10时候,没有均匀分布,碰撞几率比较大; 程序员计算器求解 : 314924944 & (10-1) = 0 314924945 & (10-1) = 1 314924946 & (10-1) = 0 314924947 & (10-1) = 1 314924948 & (10-1) = 0 314924949 & (10-1) = 1 314924950 & (10-1) = 0 314924951 & (10-1) = 1 314924952 & (10-1) = 0 结论是:数据全部分布在第一个和第二个索引位置上,大大增加了哈希碰撞的几率。效率低下
HashMap构造方法还可以手动设置初始化容量大小:
//构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap HashMap(int initialCapacity)
如果创建 HashMap对象时,手动设置的数组长度不是2的n次幂,HashMap通过位移运算和或运算得到 离那个数最近的数字2的幂次数。
//创建HashMap集合的对象,指定数组长度是10,不是2的幂 HashMap hashMap = new HashMap(10); public HashMap(int initialCapacity) {//initialCapacity=10 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=10 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity);//initialCapacity=10 } /** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) {//int cap = 10 int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
假如初始化容量设为10,最终容量会变为最近的16!
小结:
1. 根据key的hash确定存储位置时,数组长度是2的n次幂,可以保证数据的均匀插入。如果不是,会浪费数组的空间,降低集合性能!
2. 一般情况下,我们通过求余%来均匀分散数据。只不过其性能不如位运算【&】。
3. length的值为2的n次幂,hash & (length - 1) 作用完全等同于hash % length。
4. HashMap中初始化容量为2次幂原因是为了数组数据均匀分布。尽可能减少哈希冲突,提升集合性能。
5. 即便可以手动设置HashMap的初始化容量,但是最终还是会被重设为2的n次幂。