全面解读HashMap底层实现细节 在Java编程里,HashMap是极为常用的数据结构,它以键值对的形式存储数据,并且能实现高效的查找、插入和删除操作。下面,我们就深入JDK1.8的源码,全面解析HashMap的实现原理。 HashMap的基本结构 JDK1.8的HashMap采用数组加链表加红黑树的结构。数组是HashMap的主体,每个数组元素是一个链表或红黑树的头节点。当链表长度达到一定阈值(默认为8),且数组长度达到64时,链表会转换为红黑树,以提高查找效率;当红黑树节点数小于等于6时,又会转换回链表。 下面是HashMap中几个关键属性的源码:
// 默认初始容量,必须是2的幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 链表转红黑树的阈值 static final int TREEIFY_THRESHOLD = 8; // 红黑树转链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; // 转红黑树时数组的最小容量 static final int MIN_TREEIFY_CAPACITY = 64;
这里,DEFAULT_INITIAL_CAPACITY是HashMap默认的初始容量,为16;DEFAULT_LOAD_FACTOR是加载因子,当HashMap中元素数量达到容量乘以加载因子时,就会进行扩容操作。 HashMap的构造函数 HashMap有多个构造函数,下面是几个常用的构造函数及其源码解析。 无参构造函数:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 初始化加载因子为默认值 }
这个构造函数只是将加载因子初始化为默认的0.75f,并没有对数组进行初始化,数组会在第一次插入元素时进行初始化。 带初始容量参数的构造函数:
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
该构造函数调用了另一个带初始容量和加载因子参数的构造函数,加载因子使用默认值。 带初始容量和加载因子参数的构造函数:
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); }
这个构造函数会对传入的初始容量和加载因子进行合法性检查,然后将加载因子赋值给成员变量,同时通过tableSizeFor方法计算出大于等于初始容量的最小2的幂,赋值给threshold。 HashMap的put方法 put方法用于向HashMap中插入键值对,下面是put方法的源码:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put方法调用了putVal方法,其中hash(key)用于计算键的哈希值。下面是putVal方法的详细解析:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node 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 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)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; }
putVal方法的主要步骤如下:
如果数组未初始化,调用resize方法进行初始化。
计算键的哈希值对应的数组索引,如果该位置没有元素,直接插入新节点。
如果该位置有元素,判断第一个元素是否就是要插入的键,如果是,记录该节点。
如果该位置是红黑树,调用红黑树的插入方法。
如果是链表,遍历链表,找到相同的键则更新值,没有则插入到链表尾部,若链表长度达到阈值,转换为红黑树。
最后,如果元素数量超过阈值,进行扩容操作。
HashMap的get方法 get方法用于根据键获取对应的值,下面是get方法的源码:
public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
get方法调用了getNode方法,下面是getNode方法的详细解析:
final Node getNode(int hash, Object key) { Node[] tab; Node 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)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; // 未找到,返回null }
getNode方法的主要步骤如下:
判断数组是否为空,该位置是否有元素。
如果第一个元素就是要找的键,直接返回。
如果该位置是红黑树,调用红黑树的查找方法。
如果是链表,遍历链表,找到相同的键则返回,未找到则返回null。
HashMap的扩容机制 当www.ysdslt.com/HashMap中元素数量达到阈值时,会进行扩容操作,下面是resize方法的源码:
final Node[] resize() { Node[] 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; // 容量翻倍,阈值也翻倍 } 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[] newTab = (Node[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node 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)e).split(this, newTab, j, oldCap); // 该位置是红黑树,进行拆分 else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node 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方法的主要步骤如下:
计算新的容量和阈值。
创建新的数组。
将原数组中的元素迁移到新数组中,对于链表和红黑树有不同的处理方式。
返回新的数组。
通过以上对HashMap源码的详细解析,我们对JDK1.8中HashMap的实现原理有了更深入的理解,包括其基本结构、构造函数、put方法、get方法和扩容机制等。在实际开发中,合理使用HashMap能提高程序的性能。