一、定义
二、实现
1.1、HashMap
1、定义
HashMap是通过键值形式存储数据的一种集合。在jdk1.7版本,HashMap的底层数据结构基于数组+链表;在jdk1.8版本,HashMap的底层数据结构基于数组+链表+红黑树。HashMap的最大容量为2的30次方,即十亿加。
HashMap是线程不安全的集合,若要保证线程安全,需要选择Hashtable、CocurrentHashMap这些集合类,或者使用Collections.synchronizedMap()构造一个线程安全的Map。 。
注意:
- HashMap选择红黑树而不是AVL数的原因
- 红黑树插入和删除性能相对较好:红黑树在插入删除方面,相对于AVL树,它的平衡调整次数较少,性能更优,但是查询方面,由于AVL是更加严格的平衡树,所以查询比较次数更少,查询性能更好;
- 红黑树实现更加简单:相对于AVL数,红黑树在实现方面不需要维护节点的平衡因子,代码实现更加简洁;
- 红黑树占用空间更少:红黑树不需要维护节点的平衡因子,占用的空间更少。
- HashMap不直接使用红黑树的原因
在未树化阶段,链表和红黑树查询性能都差不多,但是树节点占用空间被普通节点多,所以前期使用红黑树空间利用率较低
2、特性
3、原理
(1)扩容机制
A、正常扩容
HashMap调用put方法时会判断是否进行扩容,判断依据是:哈希表大小是否等于0或者大于扩容阈值。 扩容过程是:先判断哈希表容量是否等于0,若等于0,则直接扩容到16,扩容阈值为12。若哈希表容量大于0,根据扩容阈值*2倍进行扩容。其中阈值的计算是容量乘以负载因子(默认0.75)。
B、哈希冲突时的扩容(哈希冲突的过程)
哈希冲突即多个对象的key的hashCode相同,定位到哈希表相同的位置。 当出现哈希冲突的时候,会先判断当前节点是链表节点还是树节点,若是链表节点则会先把该元素放在链表中,然后判断当前链表长度是否大于等于8,是的话会进行树化,在树化过程中,会再判断当前哈希表容量是否大于等于64,是的话,则会把链表转为红黑树,否则,则会调用扩容机制。若是树节点则会通过红黑树的方式加入。
注意:
- 若在数据量非常大的情况下,扩容是会带来性能损失的。
- 链表长度大于等于8会树化的原因:
根据概率统计,长度为8的概率小于千万分之一,所以链表长度很难达到8,树节点也很少使用。在这种情况下,链表性能已经非常差,所以在需要把链表转为红黑树,保证查询的性能。
- HashMap的负载因子设置为多少合适?
HashMap的负载因子默认是0.75,用于和哈希表容量乘积表示扩容阈值,只有大于该阈值才会进行扩容,而负载因子是可以根据应用场景和性能需求进行调整。若更注重性能,则可以降低负载因子,这样哈希表的扩容阈值变小,扩容频率变高,会消耗更多的内存,但是哈希冲突的几率降低,提高了查询速度;若更注重内存效率,可以提高负载因子,这样哈希表扩容阈值变大,扩容频率降低,内存利用率变高,但是哈希冲突的几率也会更高,查询速度也会越来越低;
(2)put方法
A、介绍
HashMap利用put方法进行添加元素,而put方法的核心方法是putVal方法。
B、put方法调用过程
- put(K key, V value)
- putVal(hash(key), key, value, false, true)
- hash(Object key)
- (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
- putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)(onlyIfAbsent表示是否不可以覆盖key,true表示不可以覆盖相同的key,默认是false)
- Node<K,V>[] tab; Node<K,V> p; int n, i
- if ((tab = table) == null || (n = tab.length) == 0)(判断哈希表是否需要扩容)
- n = (tab = resize()).length
- resize()
- Node<K,V>[] oldTab = table;(记录旧的哈希表)
- int oldCap = (oldTab == null) ? 0 : oldTab.length;(记录旧的哈希表的容量)
- int oldThr = threshold;(记录旧的哈希表的阈值)
- int newCap, newThr = 0;
- if (oldCap > 0)(计算需要扩容的大小)
- if (oldCap >= MAXIMUM_CAPACITY)(判断旧哈希表的容量是否大于等于最大容量)
- threshold = Integer.MAX_VALUE(是,则阈值修改为最大容量)
- return oldTab(返回旧的哈希表)
- else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)(判断旧容量的2倍是否小于最大容量且旧容量大于默认初始容量16)
- newThr = oldThr << 1; (是,则扩容容量为旧容量的2倍(在上面判断过程赋予给了newCap)且更新阈值为旧阈值的2倍)
- else if (oldThr > 0)(这里可能是旧容量的2倍小于默认初始容量16或者旧容量的2倍大于等于最大容量时,需要继续判断旧阈值是否大于0)
- newCap = oldThr(是,则更新新容量为旧阈值)
- else(旧阈值为0(说明未扩容过))
- newCap = DEFAULT_INITIAL_CAPACITY(设置新容量为默认初始容量16)
- newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)(设置新阈值为新容量*默认负载因子)
- if (newThr == 0)(计算初始容量新的阈值)
- float ft = (float)newCap * loadFactor
- newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE)
- threshold = newThr(更新阈值)
- Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap](创建新的哈希表)
- table = newTab
- if (oldTab != null)(判断旧的哈希表是否为空)
- for (int j = 0; j < oldCap; ++j)(不为空,把旧哈希表的每个元素设置到新哈希表
)- Node<K,V> e
- if ((e = oldTab[j]) != null)
- oldTab[j] = null;
- if (e.next == null)(普通元素)
- newTab[e.hash & (newCap - 1)] = e(普通元素:扩容后的下标只有两种情况:和旧下标相等或者旧下标+旧容量)
- else if (e instanceof TreeNode)(树节点)
- ((TreeNode<K,V>)e).split(this, newTab, j, oldCap)(将红黑树的节点进行拆分,将树中的每个节点都存入新位置,同时判断是否需要进行树转链表)
- else(遍历链表,将链表分为两部分,一部分(loHead)是索引不变,一部分(hiHead)的新索引是 oldIndex+oldCapacity,然后将链表放入对应的数组中)
- Node<K,V> loHead = null, loTail = null;
- Node<K,V> hiHead = null, hiTail = null;
- Node<K,V> next;
- do{
- next = e.next;
- if ((e.hash & oldCap) == 0)
- if (loTail == null)
- loHead = e
- else
- loTail.next = e;
- loTail = e;
- if (loTail == null)
- else
- if (hiTail == null)
- hiHead = e
- else
- hiTail.next = e
- hiTail = e
- if (hiTail == null)
- }while ((e = next) != null)
- if (loTail != null)
- loTail.next = null;
- newTab[j] = loHead;
- if (hiTail != null)
- hiTail.next = null;
- newTab[j + oldCap] = hiHead;
- for (int j = 0; j < oldCap; ++j)(不为空,把旧哈希表的每个元素设置到新哈希表
- return newTab
- if (oldCap >= MAXIMUM_CAPACITY)(判断旧哈希表的容量是否大于等于最大容量)
- resize()
- n = (tab = resize()).length
- if ((p = tab[i = (n - 1) & hash]) == null)(判断哈希表对应下标的位置是否为空)
- tab[i] = newNode(hash, key, value, null);(为空,则直接插入该元素)
- else(找到哈希表中相同key的节点,并更新到e;或者不存在相同key的节点,则需要创建一个新节点,e则为null)
- Node<K,V> e; K k;
- if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))(判断已存在节点的hash和key是否和新节点相同)
- e = p;(是,更新e值为这个相同节点)
- else if (p instanceof TreeNode)(判断已存在节点是否为树节点)
- e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
- else (已存在节点为链表节点)
- for (int binCount = 0; ; ++binCount)
- if ((e = p.next) == null)
- p.next = newNode(hash, key, value, null)
- if (binCount >= TREEIFY_THRESHOLD - 1)(判断链表是否需要树化)
- treeifyBin(tab, hash)
- break
- if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
- break
- p = e
- if ((e = p.next) == null)
- for (int binCount = 0; ; ++binCount)
- if (e != null)(哈希表中存在该key的元素,则要判断是否需要覆盖)
- V oldValue = e.value;(记录旧值)
- if (!onlyIfAbsent || oldValue == null)(判断onlyIfAbsent为false,即允许覆盖;或者判断旧值是否为空)
- e.value = value(允许覆盖,更新e的值)
- afterNodeAccess(e)
- return oldValue(成功添加,返回旧值,若是已经存在的元素,则不需要扩容)
- ++modCount
- if (++size > threshold)(判断哈希表当前大小是否大于阈值)
- resize()(是,则进行扩容)
- afterNodeInsertion(evict)
- return null(返回null)
(3)put方法大体流程
- 计算key的哈希值,若key为null,则哈希值为0;【哈希值作为参数传入putVal方法】
- 判断哈希表是否为空或者大小为0,是则进行扩容;
- 根据哈希值定位到哈希表对应的位置;
- 判断该位置是否为空;
- 为空,直接new一个节点插入到该位置;
- 不为空(找到需要替换的节点e(若不存在则需要创建e节点))
- 判断该位置是否已经存在相同的键的元素,是则更新该相同节点到e;
- 判断该位置的节点是否为树节点
- 是,则创建一个新节点赋给e,并插入到该树
- 否则,创建一个新节点赋给e,利用尾插法插入到链表
- 继续判断链表长度是否大于等于8且哈希表容量大于等于64,是则把链表转为红黑树,否则进行扩容;
- 根据传入的onlyIfAbsent值判断是否更新节点e的value,若onlyIfAbsent为false则更新节点e的value为新值,并返回旧值;为true则返回null
- 插入成功,增加modCount和size,若size超过了扩容阈值,则需要进行扩容。
(3)get方法
A、介绍
HashMap利用get方法获取对应key的值,get方法的核心方法是getNode方法。
B、方法调用过程
- get(Object key)
- (e = getNode(hash(key), key)) == null ? null : e.value
- hash(Object key)
- (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
- getNode(int hash, Object key)
- Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
- if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) (判断哈希表对应下标的节点是否存在)
- if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))(判断哈希表对应下标节点的hash和key是否相同)
- return first
- if ((e = first.next) != null)(first节点的hash和key和目标节点不相同,则需要继续遍历链表或树)
- if (first instanceof TreeNode)(判断first节点是否为树节点)
- return ((TreeNode<K,V>)first).getTreeNode(hash, key)
- do{(否则,first节点为链表节点)
- if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
- return e
- } while ((e = e.next) != null)(向后遍历链表)
- if (first instanceof TreeNode)(判断first节点是否为树节点)
- if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))(判断哈希表对应下标节点的hash和key是否相同)
- return null
- hash(Object key)
- (e = getNode(hash(key), key)) == null ? null : e.value
C、大体流程
- 计算key的哈希值,若key为null,则哈希值为0。【哈希值作为参数传入getNode】
- 判断哈希表是否为空或者大于为0,是的话,直接返回空,不是则继续后面的流程
- 根据哈希值定位到哈希表对应的位置
- 判断该位置是否为null,是则直接返回空,否则继续执行后面的流程
- 判断该位置的哈希值和key是否和目标节点一致,是则直接返回该节点,否则继续执行后面的流程
- 判断该位置的节点的下一节点是否为null,是则直接返回空,否则继续执行后面的流程
- 判断该位置的节点是否为树节点,是则通过红黑树方式获取节点信息,否则循环判断链表的每个节点是否和目标节点的哈希值和key一致,当遍历到的节点为空或者与目标节点一致则跳出循环。
1.2、TreeMap
三、线程安全的Map
1、线程安全的HashMap
(1)ConcurrentHashMap
ConcurrentHashMap是线程安全版的HashMap,它的操作和HashMap相似,只是在一些对哈希表修改的地方的处理方式有点不同。HashMap直接赋予新元素给哈希表目标下标为null的位置,而ConcurrentHashMap通过CAS方式修改哈希表目标下标的值从null到新元素。HashMap在发生哈希冲突时直接在链表或者红黑树上插入元素,而ConcurrentHashMap是通过synchronized关键字锁住发生冲突的节点再继续再链表或者红黑树上插入元素。