集合源码分析之 HashMap
一 知识准备
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
二 HashMap的数据结构:
JDK 7.0及以前
在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
JDK 8.0(本文主要介绍JDK 8.0的实现)
JDK7.0及以前,HashMap的结构都是基于一个数组以及多个链表的实现,处理Hash冲突的方法就是将对应节点以链表的形式存储。
简单的实现是以HashMap性能牺牲为代价的,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费0(N)的查找时间,严重影响性能。JDK 8.0开始使用数组+链表+红黑树的组合来实现HashMap。
(http://www.cnblogs.com/leesf456/p/5242233.html)
三 字段
//HashMap的散列表 transient Node<K,V>[] table; //存放entry的set transient Set<Map.Entry<K,V>> entrySet; //记录HashMap中存储了多少个键值对<KEY-VALUE> transient int size; //mod是modify的缩写,hashMap的结构发生结构变化时会记录一次。 transient int modCount; //默认初始化table的大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //table的最大大小 static final int MAXIMUM_CAPACITY = 1 << 30; //这是一个比例参数,当table中已经被占用的元素数与table总长度的比例不小于这个参//数的时候,就会发生table的扩容,每次扩容都以2倍大小进行扩容,注意resize()函数 static final float DEFAULT_LOAD_FACTOR = 0.75f; //当size大于这个数时,就进行一次扩容,即调用resize()函数 int threshold; //当节点冲突数达到8时,就会对hash表进行调整,如果table的长度小于64,那么会进//行table扩容,如果不小于64,那么会将因冲突形成的单链表调整为红黑树。 static final int TREEIFY_THRESHOLD = 8; //在删除冲突节点之后,同hash的节点数低于这个值时,将红黑树重新恢复为单链表。 static final int UNTREEIFY_THRESHOLD = 6; //注意到TREEIFY_THRESHOLD解释,不小于64时仅对table进行扩容,这个64就是//指这个值。 static final int MIN_TREEIFY_CAPACITY = 64;
四 构造函数
/** * @param initialCapacity 初始容量 * @param 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; this.threshold = tableSizeFor(initialCapacity); } /** * @param initialCapacity 初始容量,默认负载因子0.75*/ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 默认初始容量16,默认负载因子 0.75 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } /** * @param 使用一个map来初始化新的HashMap*/ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
五 Get 和 Put 方法
put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
putVal方法
/** * Implements Map.put and related methods * @param hash hash for key * @param key 键 * @param value 值 * @param onlyIfAbsent 如果是true,不改变已存在的值,字面意思,只当map中该对象没有才存入,默认false * @param evict 如果 false, 散列表处于创建模式,默认true * @return 返回旧值或者null或者 none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //先判断,table大小,如果table为null,或者没分配空间,就resize一次 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果首节点为null,就创建一个首节点。注意到tab[i = (n - 1) & hash],(n-1)&hash才是真正的hash值,也就是存储在table的位置(index)。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);//创建一个新的节点 else {//冲突处理 Node<K,V> e; K k; //p这时候是指向table[i]的那个Node,这时候先判断下table[i]这个节点是不是和我们待插入节点有相同的hash、key值。如果是就e = p if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //这里说明第一个节点的hash、key值与我们带插入Node的hash、key值不吻合,那么要从这个节点之后的链表节点或者树节点中查找。由于之前提到过,1.8的HashMap存储碰撞节点时
,有可能是用红黑树存储,那么先判断首节点p的类型,如果是TreeNode类型(Node的子类),那么就说明碰撞节点已经用红黑树存储,那么使用树的插入方法,如果新插入了树节点,
那么e会等于null,用于后面的判断与处理 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) {//e引用下一个节点,如果是null,表示没有找到同hash、key的节点 p.next = newNode(hash, key, value, null);//创建一个新的节点,放到冲突链表的最后 // 注意到如果这时候冲突节点个数达到8个,那么就会treeifyBin(tab, hash)函数,看是否需要改变冲突节点的存储结构,
这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果找到了同hash、key的节点,那么直接退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//调整下p节点 } } if (e != null) { // existing mapping for key //注意到这时候要判断是不是要修改已插入节点的value值,两个条件任意满足即修改 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//这个是空函数,可以由用户根据需要覆盖 return oldValue; } } ++modCount;//当插入了新节点,才会运行到这儿,由于插入了新节点,整个HashMap的结构调整次数+1 if (++size > threshold)//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容 resize(); afterNodeInsertion(evict);//这个是空函数,可以由用户根据需要覆盖 return null; }
get方法,比较简单,就是在在table上根据key.hash查找,如果hash值相同有多个,则根据key.equals()在链表或者红黑树上遍历比较,得到最终值
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
/** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> 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 && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; 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; }
六 总结
JDK 8.0的HashMap中没当冲突节点个数大于8时,就先尝试table扩容,当table数达到64后,冲突节点数为8时,则进行链表向树结构转换,这样对于冲突节点的访问复杂度就会大幅度降低,当然这是建立在插入时冲突处理算法复杂度提升为代价的。