一、HashMap数据结构
HashMap由 数组+链表+红黑树实现,桶中元素可能为链表,也可能为红黑树。为了提高综合(查询、添加、修改)效率,当桶中元素数量超过TREEIFY_THRESHOLD(默认为8)时,链表存储改为红黑树存储,当桶中元素数量小于UNTREEIFY_THRESHOLD(默认为6)时,红黑树存储改为链表存储。
table即Node<k,v>[] table,Node有两种,分别为链表节点Node和其子类TreeNode(红黑树节点)
每一个table槽称为桶,用于装hash%table.length的元素
table.length为2的整数幂,主要是为了提高取模运算效率,即对于2的整数幂n,hash%n可以转化为hash&(n-1)
二、HashMap类属性及构造函数
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; }
构造函数HashMap(int,flaot)
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小于0,否则报错 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大于最大值,否则为最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 填充因子不能小于或等于0,不能为非数字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化填充因子 this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity); }
tableSizeFor为取不小于capacity的最小2的整数幂,该算法的详解参考本文的上一篇博客
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; }
三、HashMap类的主要方法
putVal函数(put操作的基础函数)
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是否为空,如果为空,则使用扩容函数进行初始化 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 {//以下为该桶不为空的逻辑 Node<K,V> e; K k; //判断桶的第一个元素的key值是否相同(hash值相同,且能equals) //如果相同,则返回当前元素(函数末尾进行统一处理) 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; } //如果在遍历过程中,发现了key值相同,则返回当前元素(函数末尾进行统一处理) 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; //如果onlyIfAbsent为ture,则在oldValue为空时才替换 //否则直接替换 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;//修改次数+1 //map的size加1,然后判断是否达到了threshold,否则进行扩容 //threshold由Node[] table的长度及loadFactor控制 if (++size > threshold) resize(); //执行回调函数 afterNodeInsertion(evict); return null; }
put函数
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
hash函数的作用是使元素hash值更加发散,经过hash()处理之后,hash值的高16位没变,低16为原来的低16与高16异或的结果,即用16位来综合了高16位和低16位的影响,这样能提高key的发散性的主要原因是table的长度通常小于2^16,通过hash%table.length就能更发散
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
getNode函数(get操作的基础函数)
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果table不为空,则再进行查询操作 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //先检查第一个元素是否key相同 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; }
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) { //如果扩容之前的容量已经达到了最大值 //则只把threshold变成Integer.MAX_VALUE,即不限制map的最大size,之后不管插入多少元素也不触发resize if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; }//把新容量变成原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 初始capacity(构造函数输入的)被设置成了threshold newCap = oldThr; else { // oldThr=0的情况,此时表明采用默认的参数进行初始化 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); } //处理好newThr和newCap之后,开始resize()函数的真正逻辑 threshold = newThr;//设置threshold @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 { // 链式情况,此时会把原来的链分成两个链loHead和hiHead //loHead存储(e.hash & oldCap) == 0的元素 //hiHead存储(e.hash & oldCap) != 0的元素 //扩容之后,由于新的capacity为oldCap的2倍,且它们都为2的整数幂 //对于该链上的元素,如果(e.hash & oldCap) == 0,则新的槽位(hash%capacity)==旧的槽位(hash%oldCap) //否则,它们新的槽位都一样,且都为原来的槽位后移oldCap 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; }
链式情况的resize()图解:
四、对扩容的理解
扩容触发的条件是map中的元素个数达到threshold,threshold的一般值为table.length*loadFactor
扩容的作用是提高数据访问效率,通过扩容,元素会更分散,可以减少单个桶中元素的个数,进而减少链表的长度或红黑树的深度,进而可以提高数据的访问效率
扩容是一个很重的操作,需要遍历所有的元素,因此当扩容带来的性能提升优势不明显时,尽量避免扩容