HashMap源码分析(java1.8)
HashMap
首先介绍一下HashMap,它在多线程的环境下运行时线程不安全,这一点从源码中就可以看到,它没有采用任何的锁,不安全但是效率比较高。默认容量为0,如不指定容量第一次put的时候容量变为,容量必须是2的次方
相反,HashTable的方法采用了synchronized进行了同步,保证了线程的安全,但是大大降低了效率。默认的容量是11
一:HashMap中主要的属性
/** * The default initial capacity - MUST be a power of two. */ //如不指定容量,第一次集合扩容的大小,值为16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ //集合的最大容量为2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */ //默认的加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ //链表转换为红黑树的条件之一,代表链表的上的元素大于8时,此条件成立 static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ //红黑树退化成链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */ //链表转换为红黑树的第二个条件,集合中实际存在的元素个数因大于此值条件才会成立 static final int MIN_TREEIFY_CAPACITY = 64; /** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ //底层的数组 transient Node<K,V>[] table; /** * The number of key-value mappings contained in this map. */ //集合中实际元素的个数 transient int size; // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) //扩容的阈值,为加载因子*当前容量,默认是16*0.75=12, int threshold;
1.为什么默认的加载因子是0.75?
如果加载因子为1的话,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
其实源码中的注释也给出了答案,这个值是时间和空间的权衡出来的。
2.为什么默认链表转红黑树的阈值为8?
源码中其实也给出了解释,节选的注释
(http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
大致的意思就是,使用泊松分布算法加上默认的加载因子推导出,在一个hashmap中链表长度大于8的概率为0.00000006,一般的情况中其实很难达到8个key通过hash散列算法放到一个桶中。链表转红黑树只是一种应对策略。
二:HashMap的底层数据结构
jdk8对于之前的版本而言,在jdk8中hashmap的组成加入了红黑树,现在的数据结构为数组(Node[])---->单项链表---->红黑树
JDK7中使用的是Entry 到了JDK8中变为了Node,二种数据结构没有太大的区别
/** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
三:构造函数以及容量必须为2的次方的算法
1.无参构造
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() {
//赋予默认的加载因子0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
2.主要的两个带参构造
//指定初始容量,指定加载因子
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能为负数 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//初始容量如果大于最大容量,那就为最大容量,为1<<30 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能小于等于0,并且格式必须正确 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
//给一直使用的加载因子赋值 this.loadFactor = loadFactor;
//将容量变为2的次方 this.threshold = tableSizeFor(initialCapacity); } /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */
//指定默认的容量,调用带参的构造,才用默认的加载因子
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
如果设置的初始容量是个正整数,如果它不是2的整次幂,那么就会取比他大且离此数最近的一个2的次幂,就是通过tableSizeFor()进行实现的
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { 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; }
就比如传入的初始容量是23,方法运行之后他的值就会变成32
1.先将cap减一,此时n=22
2.接下来开始进行一直右移
如图所示
可以看出了,当第一次进行|=运算的时候,往后得到的二进制的后八位都是 0001 1111,这个值转换为十进制的话就是31
同时也就明白的在return 中的三元表达式中为什么取的值是n+1了
四:put方法
标红的方法会展开说明
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果hash表为空,则创建一个hash表 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果当前桶中没有元素,没有hash碰撞,直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //else代表当前桶中有节点 else { Node<K,V> e; K k; //如果桶上的key(表头)和要插入的值的key相同,那么就替换值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果当前桶上的数据结构是红黑树,那就按照红黑树的插入机制进行put else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //else代表就是链表 else { //遍历链表 for (int binCount = 0; ; ++binCount) { //如果当前遍节点的下一个节点是空,就是最后一个节点 if ((e = p.next) == null) { //那么就创建一个新节点直接插入 p.next = newNode(hash, key, value, null); //判断链表长度是否大于8,如果大于,进一步判断所有桶中元素个数是否超过了64,如果不大于64那就不会转换为红黑树,而是进行集合的扩容 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //和之前一样,代表找到了相同的key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //代表遍历链表时,找到了重复的key,在这里进行值的替换 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //操作次数 ++modCount; //将集合的size加1,判断是否超过可扩容的阈值,如超过那就扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
put的方法的主要过程就是,如果桶为空我就插入,不是空的话看桶中的头一个节点的key和我要插入的key是否相同,相同即覆盖。如果不相同看看当前桶中是否是红黑树的结构,如果是树就按照树的方式进行插入,如果不是那就是链表无疑了。开始遍历除头节点(上面判断过了)的所有节点,在判断节点中是否有key和我要插入的key相同,相同及覆盖.。没有的话找到next指针为null的元素,然后将元素插入到他的后面,JDK7是头插,JDK8是尾插
treeifyBin()方法
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //再次判断元素的个数是否大于64,不大于就扩容,大于就转为红黑树,因为扩容完链表的长度就不会超过8了 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
五:扩容方法resize()
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //获取就集合容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取扩容的阈值 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //如果旧容量超过了集合设置的最大容量,则无法扩容,扩容的阈值变为Integer.MAX_VALUE if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //新数组容量等于原数组容量左移一位,也就是之前的二倍,且要满足扩容完要小于最大的容量并且旧容量必须大于16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //初始容量设为阈值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //零初始阈值表示使用默认值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; 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; @SuppressWarnings({"rawtypes","unchecked"}) 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 { // preserve order //下面的流程就是根据高低位来进行移动 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; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
六:get()
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果哈希表不为空 && key 对应的桶上不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //桶中的第一个节点(头节点)是否就是想要的 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //判断头节点的后面是否还有节点,如果没有到下面直接return null if ((e = first.next) != null) { //如果是红黑树结构,那就按照红黑树的方式进行搜索 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //到这里就是链表 //一直遍历都最后一个元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
七:hash方法()
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
![](https://img2020.cnblogs.com/i-beta/1397021/202003/1397021-20200317173728158-883875129.png)