HashMap在java中使用的频率很高,同时也是面试时的必问的问题。今天咱们就来学习下jHashMap的源码,版本为jdk1.8。学习之前,先一起了解下HashMap的数据结构,便于理解后面所讲的内容。

HashMap的底层数据结构

由图可见,HashMap主要是由 数组+链表+红黑树 构成的。最外层是一个数组,数组中的每一个元素称作桶(segment),每个桶中存在着链表或红黑树,其中链表或红黑树中的每一个元素又称作bin。

简单的描述下put的步骤。往map中put键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置(也就是下标值),但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8时,链表自动转换为红黑树。

静态全区变量

/*** 默认初始化容量,值为16* 必须是2的n次幂.*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** 最大容量, 容量不能超出这个值。如果一个更大的初始化容量在构造函数中被指定,将被MAXIMUM_CAPACITY替换.* 必须是2的倍数。最大容量为1<<30,即2的30次方。*/
static final int MAXIMUM_CAPACITY = 1 << 30;/*** 默认的加载因子。*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 将链表转化为红黑树的临界值。* 当添加一个元素被添加到有至少TREEIFY_THRESHOLD个节点的桶中,桶中链表将被转化为树形结构。* 临界值最小为8*/
static final int TREEIFY_THRESHOLD = 8;/*** 恢复成链式结构的桶大小临界值* 小于TREEIFY_THRESHOLD,临界值最大为6*/
static final int UNTREEIFY_THRESHOLD = 6;/*** 桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。* 应该至少4*TREEIFY_THRESHOLD来避免扩容和树形结构化之间的冲突。*/
static final int MIN_TREEIFY_CAPACITY = 64;

一起走遍HashMap的流程(举个栗子)

  1. 初始化HashMap
public static void main(String[] args) {HashMap<String, String> hashMap = new HashMap<>(2);hashMap.put("java", "爪哇");String java= hashMap.get("java");System.out.println(java);}

由于我们预计会放入一个元素,出于性能考虑,我们将容量设置为 2,既保证了性能,也节约了空间

   /*** 初始化时进入的第一个方法*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** 初始化时进入的第二个方法,传入参数有(容量值,加载因子)* 流程解析:如果初始容量小于零,则抛出异常;如果初始容量大于最大容量,将最大容量值赋值给初始容量;如果加载因子小于零也会抛出异常* 接着对负载因子进行赋值,最后通过特定方法计算阀值(无论放入任何一个int 数字,都能找到离他最近的 2 的幂次方数字(并且比他大)并赋值*/public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}

上面是 HashMap 的两个构造方法,其中,我们设置了初始容量为 2, 而默认的加载因子我们之前说过:0.75,当然也可以自己设置,但 0.75 是最均衡的设置,没有特殊要求不要修改该值,加载因子过小,理论上能减少 hash 冲突,加载因子过大可以节约空间,减少 HashMap 中最耗性能的操作:reHash。

2.往HashMap中put键值对

   /*** put时进入的第一个方法*/
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** put时进入的第二个方法(计算key的hash值)* 流程解析:当key为null时,就返回零;不为null,则进入下一步计算,首先算出key的hashcode,当前key为“java”,则h=3254818,然后h* 异或h无符号右移16位的值,返回值为3254803*/
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/*** put时进入的第三个方法*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 当前对象的数组是null 或者数组长度时0时,则需要初始化数组if ((tab = table) == null || (n = tab.length) == 0)// 得到数组的长度 16n = (tab = resize()).length;// 如果通过hash值计算出的下标的地方没有元素,则根据给定的key 和 value 创建一个元素if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 如果hash冲突了Node<K,V> e; K k;// 如果给定的hash和冲突下标中的 hash 值相等并且 (已有的key和给定的key相等(地址相同,或者equals相同)),说明该key和已有的key相同if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 那么就将已存在的值赋给上面定义的e变量e = p;// 如果以存在的值是个树类型的,则将给定的键值对和该值关联。else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 如果key不相同,只是hash冲突,并且不是树,则是链表else { // 循环,直到链表中的某个节点为null,或者某个节点hash值和给定的hash值一致且key也相同,则停止循环。for (int binCount = 0; ; ++binCount) {// 如果next属性是空if ((e = p.next) == null) {// 那么创建新的节点赋值给已有的next 属性p.next = newNode(hash, key, value, null);// 如果树的阀值大于等于7,也就是,链表长度达到了8(从0开始)。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 如果链表长度达到了8,且数组长度小于64,那么就重新散列,如果大于64,则创建红黑树treeifyBin(tab, hash);// 结束循环break;}// 如果hash值和next的hash值相同且(key也相同)if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 结束循环break;// 如果给定的hash值不同或者key不同。// 将next 值赋给 p,为下次循环做铺垫p = e;}}// 通过上面的逻辑,如果e不是null,表示:该元素存在了(也就是他们呢key相等)if (e != null) { // existing mapping for key// 取出该元素的值V oldValue = e.value;// 如果 onlyIfAbsent 是 true,就不要改变已有的值,这里我们是false。// 如果是false,或者 value 是nullif (!onlyIfAbsent || oldValue == null)// 将新的值替换老的值e.value = value;// 访问后回调afterNodeAccess(e);// 返回之前的旧值return oldValue;}}// 如果e== null,需要增加 modeCount 变量,为迭代器服务。++modCount;// 如果数组长度大于了阀值if (++size > threshold)// 重新散列resize();// 插入后回调afterNodeInsertion(evict);// 返回nullreturn null;}

该方法为 HashMap 的核心方法,以下是该方法的步骤。

①判断数组是否为空,如果是空,则创建默认长度位 16 的数组。

②通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个。

③如果有元素,说明 hash 冲突了,则再次进行 3 种判断。

1.判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新e的

value,也就是替换操作。

2.如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。

3.如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度

大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。

④最后,如果这三个判断返回的 e 不为null,则说明key重复,则更新key对应的value的值。

⑤对维护着迭代器的modCount 变量加一。

⑥最后判断,如果当前数组的长度已经大于阈值了。则重新hash。

链表列下第二个菱形的条件中,加一个转为为红黑树时还要判断table.length 是否小于 MIN_TREEIFY_CAPACITY=64的条件

3.根据键get值

/***  get时进入的第一个方法* 返回指定的key映射的value,如果value为null,则返回null。*/
public V get(Object key) {Node<K,V> e;//如果通过key获取到的node为null,则返回null,否则返回node的value。getNode方法的实现就在下面。return (e = getNode(hash(key), key)) == null ? null : e.value;
}

get(E e)可以分为三个步骤:

  1. 通过hash(Object key)方法计算key的哈希值hash。
  2. 通过getNode( int hash, Object key)方法获取node。
  3. 如果node为null,返回null,否则返回node.value。
/***g et时进入的第二个方法* 根据key的哈希值和key获取对应的节点* * @param hash 指定参数key的哈希值* @param key 指定参数key* @return 返回node,如果没有则返回null*/
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//如果哈希表不为空,而且key对应的桶上不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//如果桶中的第一个节点就和指定参数hash和key匹配上了if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))//返回桶中的第一个节点return first;//如果桶中的第一个节点没有匹配上,而且有后续节点if ((e = first.next) != null) {//如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);//如果当前的桶不采用红黑树,即桶中节点结构为链式结构do {//遍历链表,直到key匹配if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}//如果哈希表为空,或者没有找到节点,返回nullreturn null;
}

getNode方法又可分为以下几个步骤:

①如果哈希表为空,或key对应的桶为空,返回null

②如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。

③如果桶中的第一个节点没有匹配上,而且有后续节点

1.如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点

2.如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配

④找到节点返回null,否则返回null。

3.resize() 扩容机制

声明一个hashmap时不给它一个容量值时,hashmap会默认的容量值为16。若声明时给定的容量值非2的n次幂,则会自动转为2的n次幂,比如初始值给的5,hashmap会自动转换为8。

如果 put值的数量大于阈值时,hashmap就会执行扩容,其中阈值为数组长度*加载因子。比如我们使用hashmap的默认容量16时,这时阈值=0.75*16=12,接着我们再put第十三个数据时,hashmap就开始扩容,扩容之后的长度为原长度的2倍,也是32。扩容就是把原来的小水桶废弃,直接用更大的水桶替换。

PS:部分图文来源网络(侵删)

hashmap为什么是2的倍数_HashMap源码解析(jdk1.8)相关推荐

  1. HashMap源码解析(JDK1.8)

    HashMap源码解析(JDK1.8) 目录 定义 构造函数 数据结构 存储实现源码分析 删除操作源码分析 hashMap遍历和异常解析 1. 定义 HashMap实现了Map接口,继承Abstrac ...

  2. Java集合-ArrayList源码解析-JDK1.8

    ◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...

  3. HashMap 源码解析(JDK1.8)

    HashMap是由数组加链表的结合体.如下图: 图中可以看出HashMap底层就是一个数组结构,每个数组中又存储着链表(链表的引用) JDK1.6实现hashmap的方式是采用位桶(数组)+链表的方式 ...

  4. ArrayBlockingQueue源码解析(JDK1.8)

    ArrayBlockingQueue是基于数组的先进先出的有界循环队列 同样我们还是先上类的关系图 从类的关系图中可以看出ArrayBlockingQueue继承一个抽象类和实现了两个接口,然后分别简 ...

  5. Android技术栈(五)HashMap(包括红黑树)与ArrayMap源码解析

    1 总览 本文会对 Android 中常用HashMap(有红黑树)和ArrayMap进行源码解析,其中 HashMap 源码来自 Android Framework API 28 (JDK=1.8) ...

  6. sqlparameter多个赋值一行完成_HashMap源码从面试题说起:请一行一行代码描述hashmap put方法...

    前言 前阵子(估计也快半年了吧)遇到这么一个面试题:请一行代码一行代码描述下HashMap put方法. 我:... 哈哈,其实也没有无语,当时知道HashMap的原理,数据结构,以及一些要注意的点, ...

  7. hashmap与concurrenthashmap源码解析

    hashmap源码解析转载:http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此 ...

  8. 通俗易懂Hashmap源码解析

    Hashmap源码解析 一.Hashmap数据结构 哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value.哈希的思路很简单 ...

  9. 【Java】HashMap的数据结构、源码解析 - 公开课笔记

    主要内容 Hashmap的数据结构 HashMap实现原理 HashMap源码解析 HashMap底层的数据结构? 1.7之前:数组+链表 1.8之后:数组+链表+红黑树 bucket 1.7之前: ...

最新文章

  1. mysql建索引 字段截取_提高MySQL索引策略一:隔离查询列
  2. linux 如何对文件解压或打包压缩
  3. php taint安装失败,PHP Taint – 一个用来检测XSS漏洞的扩展
  4. android 如何完全卸载Android Studio
  5. mybatis使用时遇到的一些问题------模糊查询、处理大于号小于号、相关函数替换空值...
  6. JAVA多线程之先行发生原则
  7. Java修炼之路——基础篇——Java集合类详解1
  8. PHP的foeach用法
  9. 是否有必要使用外键?为什么不用外键?
  10. TCP/UDP,SOCKET,HTTP,FTP 简析
  11. php社工源码,社工库源码搜集
  12. PHP动态网页设计与制作案例教程pdf
  13. a3图纸标题栏尺寸标准,a3图纸(学生a3制图标题栏尺寸)
  14. A2 雷达多点触控
  15. STM32F407+Cubemx学习应用[5]——DMA收发ModbusRS232数据——威纶通触摸屏
  16. 8.4 大学生,不要再拿兴趣说事——《逆袭大学》连载
  17. 大学生必看:基础IT技术文章300篇大合集!【包含信息/编码、IP/组网、程序逻辑、Web基础等】
  18. 20年研发管理经验谈(十七)(终结)
  19. 清除浮动2021-08-25
  20. 12306APP找回密码操作后账户被注销BUG

热门文章

  1. vue——去除白色边框
  2. android 交叉编译so,Android交叉编译htop和使用方法
  3. arcore之路-unity开发从入门到实践_Unity游戏开发——单例模式的最佳实践
  4. pq分解法中b’怎么求_14.初中数学:二元一次方程组,加减消元法怎么解?视频有详细解题步骤...
  5. UE4 动态创建Actor并且附加static mesh
  6. Boost学习之指针容器--pointer_container
  7. CE下基于Zylonite硬件平台的SD卡驱动开发
  8. 修改了WINCE自带的驱动程序后如何编译
  9. WINCE驱动程序快速入门
  10. Navicat怎么看oracle作业,[数据库] Navicat for Oracle基本用法图文介绍