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集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...
随机推荐
- Python学习--15 日期和时间
获取当前时间 # coding: utf-8 from datetime import datetime now = datetime.now() print(now) print(now.strft ...
- 《微信小程序七日谈》- 第七天:不要捡了芝麻丢了西瓜
<微信小程序七日谈>系列文章: 第一天:人生若只如初见: 第二天:你可能要抛弃原来的响应式开发思维: 第三天:玩转Page组件的生命周期: 第四天:页面路径最多五层?导航可以这么玩: 第五 ...
- 【小分享】Date对象封装,时间格式化函数time()
今天再来分享下Date的应用知识点 先看效果,类似于php里边的time('yyyy-mm-dd')用法,但是我这里没有完全依照php的参数格式来,如果有需要稍微修改一下就行. 首先,明确需要用到的参 ...
- Vuex 模块化与项目实例 (2.0)
Vuex 强调使用单一状态树,即在一个项目里只有一个 store,这个 store 集中管理了项目中所有的数据以及对数据的操作行为.但是这样带来的问题是 store 可能会非常臃肿庞大不易维护,所以就 ...
- [Usaco2014 Open Gold ]Cow Optics (树状数组+扫描线/函数式线段树)
这道题一上手就知道怎么做了= = 直接求出原光路和从目标点出发的光路,求这些光路的交点就行了 然后用树状数组+扫描线或函数式线段树就能过了= = 大量的离散+模拟+二分什么的特别恶心,考试的时候是想到 ...
- 纪中集训 Day 7
今天超级不爽啊啊啊啊 尼玛我三道题都想出来了就是没对一道,第一题没理负数尼玛题目没告诉我,第二题尼玛题目也没说最近的点是第(l+r)/2而不是距离为(a[l]+a[r])/2啊啊啊啊,第三题没打GCD ...
- Zigbee折腾之旅:(一)CC2530最小系统
最近在倒腾Zigbee,准备参加物联网全国大赛,学校有给我们发Zigbee开发板,但是对于喜欢折腾的我来说,用开发板还是不过瘾,起码也得知道怎么去画一块板子.于是乎,在百度一番后就有了下面这篇文章. ...
- 细谈sass和less中的变量及其作用域
博客原文地址:Claiyre的个人博客 https://claiyre.github.io/ 博客园地址:http://www.cnblogs.com/nuannuan7362/ 如需转载,请在文章开 ...
- js文字滚动效果实现
纯js实现,完整代码如下: <!doctype html> <html lang="en"> <head> <meta http-equi ...
- Spring IoC介绍与Bean的使用
1. 介绍 IoC IoC-Inversion of Control,即"控制反转",它不是什么技术,而是一种设计思想.在 Java 开发中, IoC意味着将设计好的对象交给容 ...