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集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...
随机推荐
- LoadRunner 调用Dll完成加密解密
LoadRunner里的函数比较少,没有MD5.Base64加密. 我们可以通过在C++里把一些加解密写成函数,供LR调用. DLL函数编写 C++里新建工程Class Library(此处是用VS2 ...
- Express之托管静态文件
中间件express.static 我们使用express初始化一个目录的时候,会在app.js中看到一大推的app.use. 其中一个主要的中间件是express.static(4.0版本依旧保留的 ...
- python3 UnicodeEncodeError: 'gbk' codec can't encode character '\uff70' in position 75267: illegal multibyte sequence
当你爬出想要的数据了,却发现中文是乱码的时候?请你计算下你内心的阴影面积 ! 菊个栗子: #!/usr/bin/env python3# -*- coding: utf-8 -*-import ioi ...
- next_permutation(全排列算法)
STL提供了两个用来计算排列组合关系的算法,分别是next_permutation和prev_permutation.首先我们必须了解什么是"下一个"排列组合,什么是"前 ...
- C++ cout 输出小数点后指定位数
在C中我们可以使用 printf("%.2lf",a);但在C++中是没有格式操作符的,该如何操作: C++使用setprecision()函数,同时必须包含头文件iomanip, ...
- Ninject之旅之十三:Ninject在ASP.NET MVC程序上的应用(附程序下载)
摘要: 在Windows客户端程序(WPF和Windows Forms)中使用Ninject和在控制台应用程序中使用Ninject没什么不同.在这些应用程序里我们不需要某些配置用来安装Ninject, ...
- context:annotation-config, mvc:annotation-driven, context:compont-scan 区别
当我们需要使用BeanPostProcessor时,直接在Spring配置文件中定义这些Bean显得比较笨拙,例如: 使用@Autowired注解,必须事先在Spring容器中声明AutowiredA ...
- Swift 面向对象解析(一)
面向对象总体概括: Swift 不仅能够面向过程编程,也能够面向对象编程(OOP).面向对象其实就是“以对象为核心”,把我们的客观世界想着是由一个个对象组成的,面向对象编程则为对象提供了属性和方法,属 ...
- ROM及其他知识
ROM--Read Only Memory 中文意思是:只读存贮器 以前的游戏机用的都是卡带,里面是一块或几块集成电路芯片,游戏程序就是在生产厂家一次性写入这几块芯片,以后用户玩游戏的时候只能读出 ...
- Kafka 0.10.1.1 特点
1.Consumer优化:心跳线程可作为后台线程,提交offset,剥离出poll函数 问题:0.10新设计的consumer是单线程的,提交offset是在poll中.本次的poll调用,提交上次p ...