JAVA源码剖析(容器篇)HashMap解析(JDK7)
Map集合:

HashMap底层结构示意图:
HashMap是一个“链表散列”,其底层是一个数组,数组里面的每一项都是一条单链表。
数组和链表中每一项存的都是一“Entry对象”,该对象内部拥有key(键值名),value(键值),next(指向下一个结点)等属性。

一、HashMap属性:
内部属性源码解析:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容器初始容量,出于hash计算时的优化的考虑,必为2^n,此处为16
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,关系到容器何时扩容
static final Entry<?,?>[] EMPTY_TABLE = {};//扩容之前的默认数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;// 用来存储数据(单向链表)的数组,长度就是容器长度即2^n
transient int size;// 当前map的key-value映射数
int threshold;// 下一次扩容的大小(第一次创建空hashmap时,会上传一个该值,用于下一次创建table时设置大小)
final float loadFactor;// 自定义的负载因子,关系到容器何时扩容
transient int modCount;// Hash表结构性修改次数,用于实现迭代器快速失败行为
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//容量阈值的默认大小
transient int hashSeed = 0;//该值在计算hash值时会用到,大小与table容量有关,由于初始table为空,所以此值初始也为空
二、HashMap构造:
构造函数poublic源码解析:
//生成空hashmap时指定容量大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//初始容量大小不能小于0
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//保证初始容量不大于默认的最大容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//负载因子不能小于0,且只能为数字
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;//设置负载因子
threshold = initialCapacity;//设置下次创建数组的大小。此时因为建的是个空map,并不会立刻通过该值初始化数组,所以这个初始化大小值会先存起来,当以后put数据后,再根据该值创建初始数组的大小
init();//进行一些必要的初始化,但在hashmap中没有实现,init()只在LinkedHashMap中有实现
}
//生成空hashmap时只指定初始容量大小,负载因子使用默认值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//生成空hashmap时,初始容量和负载因子都是用默认值
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//构造hashmap时传入一个map,负载因子使用默认值,初始容量与传入的map的大小有关
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);//对table扩容(新建一个table)
putAllForCreate(m);//将指定map的键值对添加到当前map中
}
相关private、friendly源码解析:
//将指定map的键值对添加到当前map中
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())//利用foreach迭代出传入map里的每一项entry对象
putForCreate(e.getKey(), e.getValue());//调用私有方法存入该entry。(该私有put与public的put的主要区别是,该私有put不会扩容table(因为之前已经扩过了),也不会增加modCount)
}
//存入键值对
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);//根据key值生成hash值
int i = indexFor(hash, table.length);//根据hash值生成数组下标索引
//根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);//在指定索引处的链表上创建该键值对
}
//在table数组的某一项中,创建单向链表的表头
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++;
}
Entry类源码解析:
//在数组的每一项里存储了一条单向链表,而每一条链表里存的都是这种Entry对象。key-value实际上就是被存成了这种Entry对象
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;//键值名
V value;//键值
Entry<K,V> next;//指向单向链表中的下一个entry对象
int hash;//hash值
//创建Entry对象时的构造函数
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//获取entry的key
public final K getKey() {
return key;
}
//获取entry的value
public final V getValue() {
return value;
}
//用新value覆盖旧value,并返回旧value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//如果传入的是个Entry对象,则判断其key和value是否相同,相等则返回true
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//key与value的hashcode()做^运算
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//每当相同key的value被覆盖时被调用一次,HashMap的子类LinkedHashMap中实现了这个方法
void recordAccess(HashMap<K,V> m) {
}
//每移除一个entry就调用一次
void recordRemoval(HashMap<K,V> m) {
}
}
相关说明:
1、从源码可看出构造函数主要是负责在构造时指定“初始容量”和“负载因子”。
2、如果此时创建的是一个空map,则并不会直接创建其内部table数组,而是等到有数据添加进来后再创建。
但如果此时是根据传入的新map来构建此hashmap,则会先创建一个空hashmap,再根据初始化容量创建一个table数组,之后再将传入的map里的数组存入刚刚创建的table中。
3、关于负载因子(loadFactor),表示当前容量达到总容量的什么程度时再扩容。
如,loadFactor为0.75,且设置hashmap最大容量是16。当当前的数据量达到12时(16*0.75=12),就会进行扩容。同理,如果loadFactor为1,则当前的数据容量达到16时才会扩容。
4、“初始容量”在设置时不需要设置为2^n的形式,但在使用该值扩容(创建数组)时,就会查找“大于该值的最小2^n”。
三、HashMap存储:
相关public源码解析:
//向hashmap存入一个键值对,如果该key已经存在,则覆盖该key的内容
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//如果此时数组为空,数组为空,则新建数组(扩容数组)
inflateTable(threshold);
}
if (key == null)//hashmap中允许key为null,如果key为null,则对value的存储位置特殊处理
return putForNullKey(value);
int hash = hash(key);//根据key值,生成hash值
int i = indexFor(hash, table.length);//根据hash值找到该值在table数组中的下标索引
//根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对
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))) {//如果新插入元素的key与链表中元素的key的“equals()与hashCode()”都相等,则覆盖value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//该hashmap的操作次数+1,与迭代器迭代有关
addEntry(hash, key, value, i);//运行此方法表示该key值是新key,所以将存入的键值对变成一个“Entry对象”存入数组。
return null;
}
//将指定map中的元素存入此hashmap
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();//传入map的key-value映射数量
if (numKeysToBeAdded == 0)//传入map中的映射数量为0则不存
return;
if (table == EMPTY_TABLE) {//判断当前hashmap中是否有初始table数组,没有则创建(扩容)初始table
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}
if (numKeysToBeAdded > threshold) {//传入map的key-value映射数量如果大于当前的“扩容大小”,则判断是否扩容
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)//与hashmap容量默认最大值比较,不能大于最大值
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)//如果当前数组长度小于targetCapacity,则扩大一个“2次方”
newCapacity <<= 1;//按位运算,相当于扩大了一个平方
if (newCapacity > table.length)//如果newCapacity扩大完后大于了当前数组的长度,则重新扩容数组
resize(newCapacity);
}
//foreach传入的map,将里面的每个entry对象存入此map
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
相关private、friendly源码解析:
相关说明:
四、HashMap提取:
五、HashMap遍历:
六、HashMap线程相关:
七、HashMap其他注意事项:
1、只有JDK7之前是这一套底层结构,目前最新的JDK8中HashMap当同一个hash值的节点数大于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。
JAVA源码剖析(容器篇)HashMap解析(JDK7)的更多相关文章
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- 【java集合框架源码剖析系列】java源码剖析之TreeSet
本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...
- linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】
转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
- 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)
俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
- 【java集合框架源码剖析系列】java源码剖析之LinkedList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 在实际项目中LinkedList也是使用频率非常高的一种集合,本博客将从源码角度带领大家学习关于LinkedList的知识. ...
- ThreadLocal终极源码剖析-一篇足矣!
本文较深入的分析了ThreadLocal和InheritableThreadLocal,从4个方向去分析:源码注释.源码剖析.功能测试.应用场景. 一.ThreadLocal 我们使用ThreadLo ...
- 【java集合框架源码剖析系列】java源码剖析之TreeMap
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于TreeMap的知识. 一TreeMap的定义: public class TreeMap&l ...
- 【java集合框架源码剖析系列】java源码剖析之java集合中的折半插入排序算法
注:关于排序算法,博主写过[数据结构排序算法系列]数据结构八大排序算法,基本上把所有的排序算法都详细的讲解过,而之所以单独将java集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...
随机推荐
- indexOf()--数组去重
@(JavaScript) 数组去重方法有多中,这里列举出自己认为比较容易理解的方法. 思路: 创建一个新的空数组,用来存放去重后的新数组. 利用for循环循环遍历需要去重的数组. 利用indexOf ...
- 网络请求 ---iOS
//1.url要访问的资源 NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"]; //2.请求,要向服务器请求 N ...
- iOS之网络数据下载和JSON解析
iOS之网络数据下载和JSON解析 简介 在本文中笔者将要给大家介绍IOS中如何利用NSURLconnection从网络上下载数据以及如何解析下载下来的JSON数据格式,以及如何显示数据和托图片的异步 ...
- 关于js的parseInt方式在不同浏览器下的表现
今天开发期间遇到个需求要把日期格式转换成毫秒数 日期为:2015-08-10 split之后使用parseInt将2015,08,10分别转化为数字格式. 但是使用parseInt('08')的时候却 ...
- JavaScript刷新页面的方法(包括Frame框架的刷新方式)
JavaScript刷新页面的方法 1 history.go(0) 去指定的某页 2 window.location.reload()刷新当前页面 window.location.relo ...
- 继BAT之后 第四大巨头是谁
中国互联网三大巨头的位置,毫无疑问是属于百度腾讯阿里的,但在它们之后,哪家公司能进巨头之列?京东布局不错,走亚马逊路线:360同时占据传统和移动互联网两大领域入口:小米软硬整合,生态系统完整. 很多人 ...
- EFcore与动态模型
在开发商城系统的时候,大家会遇到这样的需求,商城系统里支持多种商品类型,比如衣服,手机,首饰等,每一种产品类型都有自己独有的参数信息,比如衣服有颜色,首饰有材质等,大家可以上淘宝看一下就明白了.现在的 ...
- 每天一个linux命令(33)--du命令
Linux du命令也是查看使用空间的,但是与 df 命令不同的是 Linux du 命令是对文件和目录磁盘使用的空间的查看,还是和df 命令有一些区别的. 1.命令格式: du [选项] [文 ...
- recyclerview item点击事件
recyclerview早就不陌生了,比起过去传统的listView,样式更多,也较为高效一点,这里整理一下recylerview中item的点击事件. recyclerview和listView不同 ...
- 高精度模板 Luogu P1932 A+B & A-B & A*B & A/B Problem
P1932 A+B & A-B & A*B & A/B Problem 题目背景 这个题目很新颖吧!!! 题目描述 求A.B的和差积商余! 输入输出格式 输入格式: 两个数两行 ...