HashMap的简单源码分析(看了大佬的源码,基于1.7) put方法
参考博客: https://blog.csdn.net/eson_15/article/details/51158865
hashMap中的几个关键属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //默认初始容量是16,必须是2的幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; // aka 16 //最大容量(必须是2的幂且小于2的30次方,传入容量过大会被这个值替换) static final int MAXIMUM_CAPACITY = 1 << 30 ; //默认加载因子,所谓加载因子是指哈希表在其容量自动增加之前可以达到多满的一种尺度 static final float DEFAULT_LOAD_FACTOR = 0 .75f; //存储Entry的默认空数组 static final Entry<?,?>[] EMPTY_TABLE = {}; //存储Entry的数组,长度为2的幂。HashMap采用拉链法实现的,每个Entry的本质是个单向链表 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //HashMap的大小,即HashMap存储的键值对数量 transient int size; //HashMap的阈值,用于判断是否需要调整HashMap的容量 int threshold; //加载因子实际大小 final float loadFactor; //HashMap被修改的次数,用于fail-fast机制 transient int modCount; |
关于加载因子:
我们都知道它得底层就是一个Entry数组 命名为table, put时,先得到key的hash值,key为null时直接插入到table[0]的位置,
key不为空时,索引是怎么得到的? 哈哈,是通过table的长度和key的hash值来计算的,怎么计算的呢? 下面的 indexFor 方法 h & (length-1)
这个方法真的得解释一下(不知道解释的对不对): 索引就是通过这个算法来计算的,如果此时table的长度较短,那么索引重复的可能性就较大,反之;
所以加载因子的由来了,就是恒定一个平衡值,就跟hashCode方法中有一个平衡值31是一个道理.
大佬的解释如下:
我们主要来看看loadFactor属性,loadFactor表示Hash表中元素的填满程度。
若加载因子设置过大,则填满的元素越多,无疑空间利用率变高了,但是冲突的机会增加了,冲突的越多,链表就会变得越长,那么查找效率就会变得更低;
若加载因子设置过小,则填满的元素越少,那么空间利用率变低了,表中数据将变得更加稀疏,但是冲突的机会减小了,这样链表就不会太长,查找效率变得更高。
这看起来有点绕口,我举个简单的例子,如果数组容量为100,加载因子设置为80,即装满了80个才开始扩容,但是在装的过程中,可能有很多key对应相同的hash值,这样就会放到同一个链表中(因为没到80个不能扩容),这样就会导致很多链表都变得很长,也就是说,不同的key对应相同的hash值比数组填满到80个更加容易出现。
但是如果设置加载因子为10,那么数组填满10个就开始扩容了,10个相对来说是很容易填满的,而且在10个内出现相同的hash值概率比上面的情况要小的多,一旦扩容之后,那么计算hash值又会跟原来不一样,就不会再冲突了,这样保证了链表不会很长,甚至就一个表头都有可能,但是空间利用率很低,因为始终有很多空间没利用就开始扩容。
因此,就需要在“减小冲突”和“空间利用率”之间寻找一种平衡,这种平衡就是数据结构中有名的“时-空”矛盾的平衡。如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没什么要求的话可以将加载因子设置大一点。一般我们都使用它的默认值,即0.75。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public V put(K key, V value) { if (table == EMPTY_TABLE) { //如果哈希表没有初始化(table为空) inflateTable(threshold); //用构造时的阈值(其实就是初始容量)扩展table } //如果key==null,就将value加到table[0]的位置 //该位置永远只有一个value,新传进来的value会覆盖旧的value if (key == null ) return putForNullKey(value); int hash = hash(key); //根据键值计算hash值 int i = indexFor(hash, table.length); //搜索指定hash在table中的索引 //循环遍历Entry数组,若该key对应的键值对已经存在,则用新的value取代旧的value 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; //并返回旧的value } } modCount++; //如果在table[i]中没找到对应的key,那么就直接在该位置的链表中添加此Entry addEntry(hash, key, value, i); return null ; } //这个方法有点意思,也是为什么容量要设置为2的幂的原因 static int indexFor(int h, int length) { return h & (length-1); //得到索引的核心算法} |
table的初始化是在第一次put的时候进行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //扩展table private void inflateTable( int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); //获取和toSize最接近的2的幂作为容量 //重新计算阈值 threshold = 容量 * 加载因子 threshold = ( int ) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1 ); table = new Entry[capacity]; //用该容量初始化table initHashSeedAsNeeded(capacity); } //将初始容量转变成2的幂 例如 number = 3,返回值为4 number = 5,返回值为8 (也是为了给后面计算索引,减少索引重复率) private static int roundUpToPowerOf2( int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY //如果容量超过了最大值,设置为最大值 //否则设置为最接近给定值的2的次幂数 : (number > 1 ) ? Integer.highestOneBit((number - 1 ) << 1 ) : 1 ; } |
key为null的时候的存取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //传进key==null的Entry private V putForNullKey(V value) { for (Entry<K,V> e = table[ 0 ]; e != null ; e = e.next) { if (e.key == null ) { V oldValue = e.value; e.value = value; e.recordAccess( this ); return oldValue; } } modCount++; //如果table[0]处没有key为null addEntry( 0 , null , value, 0 ); //如果键为null的话,则hash值为0 return null ; } 今天才知道put原来还有返回值的,第一次put( null ,value1),返回值为 null ,第二次put( null ,value2),返回值为value1,recordAccess方法是把value2更新到key = null 中put别的key也是一样,不过table[ 0 ]这个位置可不仅仅存 key = null 哦,如果计算出来的索引值为 0 ,那么就得说说addEntry方法了,如果现在索引为 0 处有了一个entry,现在put一个进行,新的entry排在第一个老的entry挂在后面. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //向HashMap中添加Entry void addEntry( int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && ( null != table[bucketIndex])) { resize( 2 * table.length); //扩容2倍 hash = ( null != key) ? hash(key) : 0 ; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } //创建一个Entry void createEntry( int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //先把table中该位置原来的Entry保存 //在table中该位置新建一个Entry,将原来的Entry挂到该Entry的next table[bucketIndex] = new Entry<>(hash, key, value, e); //所以table中的每个位置永远只保存一个最新加进来的Entry,其他Entry是一个挂一个,这样挂上去的 size++; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!