HashMap源码解读,面试常考知识点速通!

前言

大家好,我是chowley,前几天面试的时候被问到了HashMap的内容,

今天我就来做一个详细的总结,帮助大家快速上手HashMap的关键点!

HashMap

在Java集合框架中,HashMap是一种常见且重要的数据结构,广泛应用于各种场景。了解其内部实现原理,不仅有助于大家更好地使用,也可以帮助我们了解面试中问题的关键点。

1. 基本概念

1.1 键值对映射

HashMap是一种通过键值对映射关系存储数据的容器。每个键对应一个值,通过键可以快速查找对应的值。

1.2 内部结构

HashMap的内部结构包括一个数组和一些链表/红黑树。数组的每个元素称为“桶”,每个桶存储着一个链表(或红黑树)的头节点。键值对通过哈希算法映射到具体的桶。

2. 哈希算法

2.1 哈希码获取

HashMap中的键通过哈希算法映射到具体的桶。键的哈希码通过调用键的hashCode方法获得。

2.2 桶的位置计算

通过一系列位运算操作,HashMap计算键的哈希码在数组中的位置,确定存储的桶。

3. 冲突解决

由于哈希算法可能导致不同的键映射到同一个桶,产生哈希冲突。HashMap通过链表和红黑树来解决冲突。当桶中的链表长度达到一定阈值时,链表会转换成红黑树,以提高查询效率。

4. 常用方法源码分析

4.1 构造方法

    // 无参构造方法public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用默认负载因子}// 接受另一个Map作为参数public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}// 指定初始容量大小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;// 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化this.threshold = tableSizeFor(initialCapacity);}

HashMap的构造方法主要设置了默认的初始容量、最大容量、负载因子等参数,以及初始化了一些阈值。

4.2 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;// 如果数组为空或长度为0,则进行初始化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)treeifyBin(tab, hash);break;}// 如果找到相同的键,则更新值if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 如果找到相同的键,更新值if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// 结构性修改计数器增加++modCount;// 如果元素个数超过阈值,进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

put方法根据键的哈希值计算桶的位置,然后根据当前桶中的元素情况,选择合适的方式进行插入,包括链表插入和红黑树插入。如果插入后元素个数超过阈值,触发扩容操作。

4.3 get方法

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}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 &&((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果桶中是树形结构,调用树形查找方法if ((e = first.next) 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;
}

get方法根据键的哈希值计算桶的位置,然后根据桶中的元素情况,选择合适的方式进行查找,包括链表查找和红黑树查找。

4.4 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) {// 如果旧数组已经达到最大容量,则不再扩容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 thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 如果新的阈值为0,计算新的阈值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 orderNode<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;elseloTail.next = e;loTail = e;} else {if (hiTail == null)hiHead = e;elsehiTail.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进行扩容。它根据当前的数组容量和扩容阈值,计算新的容量和新的扩容阈值,并创建新的数组。然后,将旧数组中的元素根据其哈希值重新分配到新数组中。

总结

通过对HashMap源码的简要解读,我们可以了解到其内部实现采用数组+链表/红黑树的方式,通过哈希算法将键映射到具体的桶中。在插入元素时,会根据桶中的结构选择合适的方式进行插入。

同时,HashMap具有动态扩容机制,当元素个数超过一定阈值时,会触发扩容操作。深入了解这些源码细节有助于更好地理解HashMap的工作原理。

好了,以上就是本文的全部内容,如有问题欢迎留言讨论。

我是chowley,一个专注互联网技术和软件质量保障领域的博主,我们下次再见!

欢迎点赞、评论、收藏,it's important for me.

欢迎点赞、评论、收藏,it's important for me.

欢迎点赞、评论、收藏,it's important for me.