这里主要讲HashMap的三个操作put和get和remove,当然这三个操作基本涵括了所有源码。
先给出HashMap的一些属性和HashMap的构造器:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量为16 //Map的最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //当链表结点数>=8且Map容量大于64时,会将链表转换成红黑树 static final int TREEIFY_THRESHOLD = 8; static final int MIN_TREEIFY_CAPACITY = 64; //当红黑树的结点数<=6时会退化成链表 static final int UNTREEIFY_THRESHOLD = 6;
//如果Map中已经使用的桶树>=threshold这个阀值时会自动扩容,扩容后的阀值和容量都为之前的2倍
int threshold;
//真正存放元素的东东 每一个位置称之为桶
transient Node<K,V>[] table;
//负载因子 决定了阀值 threshold=table.length*loadFactor
final float loadFactor;
//指定初始化容量和负载因子
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;
//这个函数的作用待会说 需要说的是threshold
//这里的threshold不是真正的阀值 当真正初始化时 table.length=threshold 然后threshold=table.length(真正的容量)*负载因子
this.threshold = tableSizeFor(initialCapacity); }
/返回一个数num,其中num>=cap,且num=2^n //思路:将二进制表示中最高位为1的位的右边全置为1 static final int tableSizeFor(int cap) { int n = cap - 1;//如果cap本身就是 a power of two n |= n >>> 1;//将最高位为1的右边1位置为1 n |= n >>> 2;//右边3位置为1,if exist n |= n >>> 4;//右边7位置为1,if exist n |= n >>> 8;//右边14位置为1,if exist n |= n >>> 16;//右边31为置为1,if exist return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }public HashMap( int initialCapacity) { this(initialCapacity , DEFAULT_LOAD_FACTOR) ;}
//在容器真正初始化时的容量为16
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }当你new出一个HashMap对象之后,容器其实并没有初始化,当你第一次使用put操作时,才完成容器的初始化。
那么接下来分析put操作:
首先给出内部桶的数据结构(链表结点)
//内部桶的表示 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K, V> next; /*....*/ }
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
//这里先不要纠结onlyIfAbsent和evict则两个参数 第一个参数指定了 当桶中 已装有与待插入结点的hash和key相同的结点时 的操作策略
//evict与LinkHashMap扩展HashMap的某些实现有关。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果容器未初始化 则先初始化 待会再分析resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果hash值对应的桶未被使用 索引为(n-1)&hash 得出的值:0<=i<=n-1 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //对应的桶已经存在元素 else { Node<K,V> e; K k; //如果出现同样的key则替换旧的 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 { //用链地址法处理冲突 如果链表的长度大于树化阀值TREEIFY_THRESHOLD //可能会转换成红黑树(注意是可能) 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; } //链表中存在着与待插入结点 key和hash相同的结点,则e指向这个相同的结点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // 如果出现同样的key则替换旧的 返回旧的value V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //LinkHashMap继承Hashmap 这个方法是留给LinkedHashMap实现的 用于记录插入顺序 afterNodeAccess(e); return oldValue; } } ++modCount; //如果当前已使用的桶大于阀值则进行扩容 if (++size > threshold) resize(); //LinkedHashMap继承Hashmap 这个方法是留给LinkedHashMap实现的 用于记录插入顺序 afterNodeInsertion(evict); return null; }
接下来分析putVal用到的各个方法。
//扩容或者初始化HashMap 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 } //对应 new Hashmap(cap) 第一次调用put时 设置容量 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //对应new Hashmap() 第一次使用(调用put)的初始化 设置容量和阀值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //对应 new Hashmap(cap) 设置阀值 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; //如果oldTab为null的话直接返回,即容器第一次使用 不然将oldTap的结点copy到newTab if (oldTab != null) { //遍历oldTab中的每个结点 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //如果该结点不为null 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); //该桶装的是链表 链表中的各个元素的hash值可能不同 else { // preserve order 分类构造两个链表 是为了保证链表在新容器中的顺序 //将链表结点分为两类: //一类为新下标分析putVal用到的treeifyBin()=oldCap //第一类结点的头指针和尾指针 Node<K,V> loHead = null, loTail = null; //第二类结点的头指针和尾指针 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //第一类结点 hash&oldCap=0 的结点 新下标if ((e.hash & oldCap) == 0) { if (loTail == null) { loHead = e; } else { loTail.next = e; } loTail = e; } //第二类结点 hash&oldCap!=0 的结点 新下标>=oldCap else { if (hiTail == null) { hiHead = e; } else { hiTail.next = e; } hiTail = e; } } while ((e = next) != null); //第一类结点 因为oldCap=2^n 所以hash值的第n位一定为0 // 所以这一类结点在新容器中的下标与老容器中的一样(用位操作证明) if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //第二类结点 即hash值的第n位一定为1 // 所以这一类结点在新容器中的下标=j+oldCap if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } //我想吐槽一下这里的实现 用hash&oldCap==0?来分类. //换成这样会不会更好? (hash&newCap)>=oldCap? } } } } return newTab; }
先给出红黑树结点的数据结构:这里先暂时无视它继承的LinkHashMap.Entry,读者可以认为它继承前面说的Node
本文不会过多涉及红黑树的具体实现(增删改查)会涉及一些辅助方法,有兴趣的读者可以去参考其他一些红黑树的资料。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion 当红黑树退化成链表时会用到 boolean red;
}
//用于判断对应结点是否转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //如果容量小于最小树化要求的容量(64)会进行一次扩容处理
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { //将原来桶对应的链表转换成树结点 并用设置结点的prev和next属性来保持链表的顺序 //使得转成红黑树后也可以退化成链表 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); //将桶的结点替换为树结点 并调用treefy来完成红黑树的构造 if ((tab[index] = hd) != null) hd.treeify(tab); } }
接下来涉及红黑树的一些辅助方法但不会涉及红黑树的增删改查和平衡调整什么的(如果读者并不理解红黑树的实现,接下来的几个辅助方法也可先暂时不看),不然就显得本末倒置了。
分析上面那个treeifyBin()用到的terrify()方法:
//传入tab参数是为了将转换后的红黑树的根节点放到对应的桶中(因为转换过后的红黑树的根节点可能不在桶中) final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; //root是树根结点 x是待插入结点 for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; //将根节点的parent置为null if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class> kc = null; //二分查找合适的插入位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; //位于同一个桶中的结点的hash值可能不同 //优先依靠hash值来比较大小 if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; //x.hash=p.hash //如果待插入结点x的key没有实现了Comparable接口或者 //已经实现Comparable接口但是,x的key与父结点的key不是同一个类的实例 //则dir为 内定的比较规则计算得出的结果 //否则dir为待插入结点key与父结点key的比较结果(即正常情况 key都为同一个类的实例且实现Comparable接口) else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; //找到要插入的位置后 进行红黑树的插入 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; //红黑树的插入 有兴趣自己看 root = balanceInsertion(root, x); break; } } } } //将已经平衡的红黑树的根节点放入桶 moveRootToFront(tab, root); }分析前面putVal()方法用到的putTreeVal()
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { Class> kc = null; boolean searched = false; //如果该节点(this)不是树根结点 则调用root(),令root为树根结点 TreeNode<K,V> root = (parent != null) ? root() : this; //接下来的插入规则与treeify()如出一辙 //二分查找合适的插入位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk; //dir为比较的结果,比较的值为hash值 if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; //即树中已经存在相同元素 则直接返回 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; //x.hash=p.hash //如果待插入结点x的key没有实现了Comparable接口或者 //已经实现Comparable接口但是,x的key与父结点的key不是同一个类的实例 //则dir为 内定的比较规则计算得出的结果 //否则dir为待插入结点key与父结点key的比较结果(即正常情况 key都为同一个类的实例且实现Comparable接口) else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode<K,V> q, ch; searched = true; //即树中已经存在相同元素 则直接返回 if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } //采用内定的方法计算比较结果 dir = tieBreakOrder(k, pk); } TreeNode<K,V> xp = p; //找到要插入的位置后 进行红黑树的插入 if ((p = (dir <= 0) ? p.left : p.right) == null) { Node<K,V> xpn = xp.next; TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); if (dir <= 0) xp.left = x; else xp.right = x; //next与prev用于保证 当红黑树退化时,能够还原成原来的链表(保证顺序) xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode<K,V>)xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); return null; } } }分析resize用到的split()方法:
//在resize()扩容,从oldTab迁移到newTab时,当要迁移的结点为树结点 //则调用此方法完成迁移 这里假设你已经读完了resize()方法. final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { //首先将树节点 链接成两个链表(树的结构不变) 因为TreeNode继承了Node 所以也可以表示为链表而树结构不变 //按照resize()中链表的分类法来构造这个两个链表 TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; //lc为第一类链表的元素个数 hc为第二类链表的元素个数 //用法详见后续说明 int lc = 0, hc = 0; for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } //lc和hc的作用显现在这里 if (loHead != null) { //如果链表长度lc<=6(UNTREEIFY_THSHOLD) 那么 //将红黑树退化成链表 if (lc <= UNTREEIFY_THRESHOLD) //untreeify()很简单不展开了 tab[index] = loHead.untreeify(map); else { tab[index] = loHead; if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } //如果链表长度hc<=6(UNTREEIFY_THSHOLD) 那么 //将红黑树退化成链表 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
至此 put方法全部分析完毕 有空再分析get()和remove()方法。