【Explore SRC】一起看看HashMap源码(个人笔记)
HashMap源码一直是众多Java程序员的必经之路,今天我也看看,大家凑热闹不?基于水平有限,有些地方理解错误、理解不了,请大家指出哦~~
版本说明
查看的版本是jdk1.7.0_71
结构概要图
从构造方法看起吧
public HashMap(int initialCapacity, float loadFactor) public HashMap(int initialCapacity) public HashMap() public HashMap(Map<? extends K, ? extends V> m)
HashMap有4个构造方法,具体看下代码,可知第2、3个方法都是调用第1个方法进行操作的。那么,具体看第1个吧。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final float DEFAULT_LOAD_FACTOR = 0.75f;
查看参数的全局变量,知道初始化容量是16,扩容因子(容量达到哪里时要重新构造HashMap的容器)默认为0.75。
最后具体看第1个方法的方法体,主要作了3件事:
1、如果入参异常,则抛出异常
2、对初始化容量进行饱顶
3、将入参设置为属性,这里有点注意:threshold(阀值),在HashMap刚初始化时被赋值为初始容量。
4、后面,还调用了init(),此方法是空方法体的,供子类的开发人员扩展
哪些方法常用,当然是上子弹的方法了--put(K key, V value)
if (table == EMPTY_TABLE) { inflateTable(threshold); }
......
/** * Inflates the table. */ private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
内部有个叫table的数组,其元素指向的是单向链表,而这链表装载的是Entry类(内部类,实现Map.Entry),实际主要包含每个元素的数据结构,比如key、value、next指针等。
刚刚初始化HashMap时,此时table为空,这时就需要根据threshold对table进行扩容。
将table扩容至threshold的上随2的n次方大小。比如,threshold为16,则扩容至16;threshold为17,则扩容至32。
注:
roundUpToPowerOf2()见下述。
if (key == null) return putForNullKey(value);
查看putForNullKey方法,将key为null的元素,放入table下表为0的链表里。而逻辑与下面要讲的放入元素的逻辑基本一致。
注:
为什么是下标为0的元素放key为null的值呢?见下述。
int hash = hash(key); int i = indexFor(hash, table.length);
对key对象进行哈希计算后,映射到table数组中一个位置,为i。
注:
indexFor(),见下述。
for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
上面已找到当前要插入的元素位于table数组的哪个位置了,接下来就线性遍历这个位置指向的链表,如果发现hash值相等并且key也相等的,就说明此Map已包含此元素,那么,就用新值覆盖旧值,并返回旧值吧。
modCount++; addEntry(hash, key, value, i); ... /** * Adds a new entry with the specified key, value and hash code to * the specified bucket. It is the responsibility of this * method to resize the table if appropriate. * * Subclass overrides this to alter the behavior of put method. */ void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } /** * Like addEntry except that this version is used when creating entries * as part of Map construction or "pseudo-construction" (cloning, * deserialization). This version needn't worry about resizing the table. * * Subclass overrides this to alter the behavior of HashMap(Map), * clone, and readObject. */ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
程序跑到这里,说明在Map中并没有找到Key值,需要作插入。
modCount是记录插入的次数,估计用作限制并发操作的。
addEntry()在插入元素前,要判断元素是否达到一个阀值,如果达到,就对table进行2倍的扩容、重新哈希。(此点内容下面讲述)
然后,重新计算元素在扩容后的位置,调用createEntry()作实际的插入操作。插入操作,就是将新插入的元素的next指向链表的第一个元素,然后将table数字的该下表指向新插入的元素。
/** * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). */ void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } /** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
先根据newCapacity实例化一个新的table。
因新table的长度变更了嘛,需遍历原table所指向的链表的所有元素,一个个转到新的table(计算hash、重新定位)。(至于是否重新hash,我还没看明白)
细节
roundUpToPowerOf2(int number)
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
这个方法是计算number最接近的2的N次方数。
其中Integer.highestOneBit()是取最高位1对应的数,如果是正数,返回的是最接近的比它小的2的N次方;如果是负数,返回的是-2147483648,即Integer的最小值。
那为什么要先减1,再求highestOneBit()?
举几个数的二进制就知道了:
00001111 = 15 -> 00011110 = 30 -> highestOneBit(30) = 16
00010000 = 16 -> 00100000 = 32 -> highestOneBit(32) = 32
所以,为了获取number最接近的2的N次方数,就先减一。
附一个简单的分解计算:

public class Lefter { public static void main(String[] args) { for (int i = 2; i <= 17; i++) { System.out.println(i); System.out.println(i - 1); System.out.println((i - 1) << 1); System.out.println(Integer.highestOneBit((i - 1) << 1)); System.out.println("result : " + i + " -> " + Integer.highestOneBit((i - 1) << 1)); } } }
结果:

2 1 2 2 result : 2 -> 2 3 2 4 4 result : 3 -> 4 4 3 6 4 result : 4 -> 4 5 4 8 8 result : 5 -> 8 6 5 10 8 result : 6 -> 8 7 6 12 8 result : 7 -> 8 8 7 14 8 result : 8 -> 8 9 8 16 16 result : 9 -> 16 10 9 18 16 result : 10 -> 16 11 10 20 16 result : 11 -> 16 12 11 22 16 result : 12 -> 16 13 12 24 16 result : 13 -> 16 14 13 26 16 result : 14 -> 16 15 14 28 16 result : 15 -> 16 16 15 30 16 result : 16 -> 16 17 16 32 32 result : 17 -> 32
indexFor(int h, int length)
将h映射到length的范围里,效果就像求模。
return h & (length-1);
将h和length - 1和操作就可以了。
比如length为16,那么:
16 = 00010000
15 = 00001111
为什么是下标为0的元素放key为null的值呢?
根据上述indexFor(int h, int length)映射的范围在1到length - 1,那么剩下的下标就是0。
为什么hash数组的长度要弄成2的N次方?
要将散列值映射到一定范围内,一般来说有2种方法,一是求模,二是与2的N次方值作&运算。而现代CPU对除法、求模运算的效率不算高,所以用第二种方法会效率比较高,所以数组被设计为2的N次方。
剩下的仍未想明白
1、initHashSeedAsNeeded(capacity)
2、hash()
本博客为学习、笔记之用,以笔记形式记录学习的知识与感悟。学习过程中可能参考各种资料,如觉文中表述过分引用,请务必告知,以便迅速处理。如有错漏,不吝赐教。
如果本文对您有用,点赞或评论哦;如果您喜欢我的文章,请点击关注我哦~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用