HashMap的简单源码分析(看了大佬的源码,基于1.7) put方法
参考博客: https://blog.csdn.net/eson_15/article/details/51158865
hashMap中的几个关键属性
//默认初始容量是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。
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的时候进行的
//扩展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的时候的存取:
//传进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挂在后面.
//向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++;
}
HashMap的简单源码分析(看了大佬的源码,基于1.7) put方法的更多相关文章
- Tomcat源码分析一:编译Tomcat源码
Tomcat源码分析一:编译Tomcat源码 1 内容介绍 在之前的<Servlet与Tomcat运行示例>一文中,给大家带来如何在Tomcat中部署Servlet应用的相关步骤,本文将就 ...
- Android源码分析(十一)-----Android源码中如何引用aar文件
一:aar文件如何引用 系统Settings中引用bidehelper-1.1.12.aar 文件为例 源码地址:packages/apps/Settings/Android.mk LOCAL_PAT ...
- AQS源码分析看这一篇就够了
好了,我们来开始今天的内容,首先我们来看下AQS是什么,全称是 AbstractQueuedSynchronizer 翻译过来就是[抽象队列同步]对吧.通过名字我们也能看出这是个抽象类 而且里面定 ...
- 第九篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 cache table
/** Spark SQL源码分析系列文章*/ Spark SQL 可以将数据缓存到内存中,我们可以见到的通过调用cache table tableName即可将一张表缓存到内存中,来极大的提高查询效 ...
- JDK1.8源码分析03之idea搭建源码阅读环境
序言:上一节说了阅读源码的顺序,有了一个大体的方向,咱们就知道该如何下手.接下来,就要搭建一个方便阅读源码及debug的环境.有助于跟踪源码的调用情况. 目前新开发的项目, 大多数都是基于JDK1.8 ...
- 基于个人理解的springAOP部分源码分析,内含较多源码,慎入
本文源码较多,讲述一些个人对spring中AOP编程的一个源码分析理解,只代表个人理解,希望能和大家进行交流,有什么错误也渴求指点!!!接下来进入正题 AOP的实现,我认为简单的说就是利用代理模式,对 ...
- 4 weekend110的textinputformat对切片规划的源码分析 + 倒排索引的mr实现 + 多个job在同一个main方法中提交
好的,现在,来weekend110的textinputformat对切片规划的源码分析, Inputformat默认是textinputformat,一通百通. 这就是今天,weekend110的te ...
- Java容器类源码分析之Iterator与ListIterator迭代器(基于JDK8)
一.基本概念 迭代器是一个对象,也是一种设计模式,Java有两个用来实实现迭代器的接口,分别是Iterator接口和继承自Iterator的ListIterator接口.实现迭代器接口的类的对象有遍历 ...
- 第十篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 query
/** Spark SQL源码分析系列文章*/ 前面讲到了Spark SQL In-Memory Columnar Storage的存储结构是基于列存储的. 那么基于以上存储结构,我们查询cache在 ...
随机推荐
- c# @符号后面对 双引号转义
本文讲述c#中如何转义双引号. c#中转义双引号",使用的转义字符仍然是\. string str = "\"www.itjsxx.com\""; 但 ...
- redis php 实例二
前面一篇博客主要是string类型,list类型和set类型,下面hash类型和zset类型 1,hset 描述:将哈希表key中的域field的值设为value.如果key不存在,一个新的哈希表被创 ...
- 基于字典SR各种方法【稀疏编码多种方法】
基于字典的图像超分辨率实现 - CSDN博客 http://blog.csdn.net/u011630458/article/details/65635155 简介 这段时间在看基于字典的单帧图像超分 ...
- 【ask】vc11 sln文件里加入新的vcxproj已有vcxproj里的用户宏没有自动复制过来
今天在quick-cocos2d-x生成的sln,需要添加一个新的自己的lib,以前就是直接把.h和.cc文件直接添加过去(为了省事呗),今天时间宽裕索性还是新建一个vcxproj吧,然后用静态lib ...
- Linux shell 1-初步认识
1.什么是linux linux是一种操作系统,它可划分为以下四部分 1.linux内核(Linux系统的核心,负责管理系统内存,硬件驱动,文件系统等) 2.GNU工具(一组软件工具,提供一些类UNI ...
- Cannot call sendRedirect() after the response has been committed错误;
Cannot call sendRedirect() after the response has been committed提示信息其实很清楚,如果response已经提交过了,就无法再发送sen ...
- InetAddress类和InetSocketAddress的使用
一简介 InetAddress是Java对IP地址的封装,代表互联网协议(IP)地址:InetAddress对象的获取只能通过静态方法,比如根据主机名获取主机的ip地址封装对象: ? 1 InetAd ...
- 170209、mysql索引的建立
用到索引最普通的情况,是为出现在where子句的字段建一个索引.为方便讲述,我们先建立一个如下的表. Code代码如下: CREATE TABLE mytable ( id serial primar ...
- 封装IP池和用户代理相应的类(python3)
一.middlewares.py源代码: # -*- coding: utf-8 -*- # 导入随机模块 import random # 导入有关IP池有关的模块 from scrapy.contr ...
- 推荐10 个短小却超实用的 JavaScript 代码段
1. 判断日期是否有效 JavaScript中自带的日期函数还是太过简单,很难满足真实项目中对不同日期格式进行解析和判断的需要.jQuery也有一些第三方库来使日期相关的处理变得简单,但有时你可能只需 ...