HashMap源码解析:
属性解析:
HashMap的默认容量,当没有指定容量大小时且扩容阀值没有发生变化时使用默认的容量大小初始化一个node数组(table)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最大的容量,若当扩容时扩容的大小超过该最大容量则按该容量算
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的加载因子,加载因子的作用是由于计算扩容的阀值=capacity*loadfactory
static final float DEFAULT_LOAD_FACTOR = 0.75f;
树化的阈值,hash冲突时首先会形成一个链表,冲突的元素会被添加到链表的尾部;当这个链表的长度达到这个数量时会转化成一个树(红黑树)默认树化的阈值为8
static final int TREEIFY_THRESHOLD = 8;
当树上的元素数量大小小于一个阈值时由树转换成一个链表,默认6
static final int UNTREEIFY_THRESHOLD = 6;
最小的树化容量,若发生树化操作时发现容量大小还未超过这个值则会认为是因为容量太小而导致冲突的概率增加,所以首先需要扩容,默认64
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap有4个构造方法
1.无参构造:
指定加载因子为默认的加载因子;
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
指定容量和加载因子 或是只指定容量的
两者都会计算一个扩容阈值threshold,这个阈值会在第一次初始化node数组(table)时用来当做数组的长度;这个阈值必须是2的指数(涉及到hash映射的原理,在进行对key的hash映射到node数组上(table)的操作是(不是源码,但是属于同一个意思)
n=table.length;index=(n-1)&hash(key);table[index]=e;
这样的操作可以保证不会发生数组越界的异常;
public HashMap(int initialCapacity, float loadFactor) { 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); }
public HashMap(int initialCapacity, float loadFactor) { 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); }
最后一个构造函数传入一个Map对象,此时加载因子用默认的加载因子替代;随后会将this.threshold设置成map.size/loadfactory这样在第一次初始化时就会将table的大小设置成this.threshold和将this.threshold设置成map.size;这样做的原理是保证在数据拷贝的时候只会对table扩容一次;
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
先来解析HashMap的核心的扩容操作
扩容操作在第一次resize也就是(table=null)时进行初始化操作当没有指定阈值(threshold)时使用默认的初始化容量,否则把容量设置成该阈值(threshold);
当该操作发生在第一次resize后则把容量和阈值翻倍;
扩容会重新创建一个Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
创建新的table后如果oldTable!=null则要把oldTable中的数据浅拷贝到newTable;第一步会把oldTable中的引用设置为null,利于垃圾收集器的回收;第二步会判断该位置上有没有放置元素,若有则会进行判断元素是否有next节点,没有则直接将该元素放置newTable,然后进行下一个位置判断;否则再判断该位置的元素是否是ThreeNode,若是则会进行一个对树的拆分操作:((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
此操作可能会对该树进行解除树操作(当树上的元素数量小于解除树的数量阈值时);
否则该元素一定是 一个链表的头结点,接下来会将这个链表拆分成两个链表;1个链表是在oldTable中的映射和在newTable中的映射相同,另一个链表是不同;(通过e.hash&oldCapacity==0)来判断该元素在oldTable的映射和在newTable的映射是否相同;最后判断完链表上的所有元素后会将这两个链表分别放入newTable的原位置和原位置索引+oldTable.size的位置;
这样整个扩容操作完成
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) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } 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; }
再来看HashMap的常规操作:
在分析之前我们先看看获取一个key的hash值方法:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
次方法会将key的hash值的二进制进行一个“降位”操作即当key的hash值的二进制的1都分布在高位时将二进制高位中的“1”复制到低位,由于一般使用HashMap时容量都不会超过2的16次方数量而当进行
(length-1)&key时发生hash冲突的概率增大,所以这样做的目的是为了缓解hash冲突的一个方法;
put方法:put方法将功能代理到putValue方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
当table=null时即未初始化时会进行一次resize方法进行初始化操作;随后根据映射规则:(length-1)&hash;判断次索引位置上是否有元素,如果没有元素则直接放置到此位置上;否则可能发生了映射冲突或者是重复了,若是重复了则根据参数决定是否覆盖该value;若是发生映射冲突则首先判断该node是否是treenode实例;若是则将该node加入到树中,若不是则需要将该node加入到链表尾部在加入到链表的尾部过程中会判断是否达到了treeify的阈值,如果达到了则会将该链表转换成一颗红黑树;
如果发生了重复数据添加则不会进行++modCount;操作和容量判断操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; 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) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
来分析一下HashMap的get方法:
get方法将功能代理到getNode方法:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
该方法首先会判断该表是否是一个空表或是在表上该映射是否有数据;若不符合条件则返回null;
符合条件后会查找与该key符合的node,查找条件是node中的hash值要和hash(key)值相等并且node.key=需要查找的key或是node.key.equals(需要查找的key);
if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
若是该映射的索引上有元素但是第一个元素又不是我们需要查找的元素则判断该位置是否存在hash冲突;如果存在冲突:若该node是TreeNode的实例则用树的方式获取node;否则通过链表的方式查找元素;其中判断key是否相等的条件是
if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))
这里先进行hash值的判断是进行一次简单的筛选,可以加速查找的速率;真正的判断条件在后面,两个key的内存地址相等即两个key属于同一个对象,或者key类型重写了equals方法(比如Integer),则再次用equals判断是否相等;所以这里可以得出一个结论:在使用HashMap的时候,如果有必要可以重写key类型的hashCode方法和equals方法;
再来看一下HashMap的迭代器:
abstract class HashIterator { Node<K,V> next; // next entry to return Node<K,V> current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }
首先分析它的一个构造函数:主要是记录一下用于快速失败的expectedModCount ,和找到下一个(next)需要迭代的元素;然后nextNode方法用于返回此次迭代的元素和找到下一个需要迭代的元素,查找的顺序是按表中的索引从小到大开始,如果遇到了hash冲突则会先迭代冲突的元素;迭代器里还提供了remove操作,所以当需要在迭代过程中使用移除操作时可以试用迭代器自带的操作;
迭代器并不能完全保证当出现并发修改的时候一定会抛出异常,一般仅适用于defect bug,例如在调用iterator的remove方法时,会对MODCount的数量进行一个同步,如果在检查和同步modCount之间其它线程对modCount进行修改是无法发现;