Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 JDK1.7 和 1.8 中具体实现稍有不同。

今天我们只讲解JDK1.7版本的HashMap。

1、HashMap的数据结构图

是一个数组+链表结构

2、HashMap成员变量

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 16;/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry<K,V>[] table;/**
 * The number of key-value mappings contained in this map.
 */
transient int size;/**
 * The next size value at which to resize (capacity * load factor).
 * @serial
 */
int threshold;/**
 * The load factor for the hash table.
 *
 * @serial
 */
final float loadFactor;

这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?

① DEFAULT_INITIAL_CAPACITY :初始化桶大小(16),因为底层是数组,所以这是数组默认的大小。

② MAXIMUM_CAPACITY :桶最大值。

③ DEFAULT_LOAD_FACTOR :默认的负载因子(0.75)

④ table:真正存放数据的数组。

⑤ size:map中存放的键值对的数量。

⑥ threshold:resize扩容时的阈值。

⑦ loadFactor:负载因子,可在初始化时显式指定。

HashMap 的构造函数可以指定参数也可以无参:

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table = new Entry[DEFAULT_INITIAL_CAPACITY];init();
}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}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);// Find a power of 2 >= initialCapacity
    int capacity = 1;while (capacity < initialCapacity)capacity <<= 1;this.loadFactor = loadFactor;threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);table = new Entry[capacity];useAltHashing = sun.misc.VM.isBooted() &&(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);init();
}

默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。

3、Entry类

根据代码可以看到真正存放数据的是:transient Entry<K,V>[] table,这个数组,那么它又是如何定义的呢?

static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;/**
     * Creates new entry.
     */Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}......}

Entry 是 HashMap 中的一个内部类,从它的成员变量很容易看出:

① key 就是写入时的键。

② value 自然就是值。

③ 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。

④ hash 存放的是当前 key 的 hashcode。

知晓了基本结构,那来看看其中重要的put、get方法。

4、put 方法

public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);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;}}modCount++;addEntry(hash, key, value, i);return null;
}

① 如果 key 为空,则 put 一个空值进去。

② 根据 key 计算出 hashcode。

③ 根据计算出的 hashcode 定位出所在桶。

④ 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。

⑤ 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。

void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, bucketIndex);
}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++;
}

① 当调用 addEntry 写入 Entry 时需要判断是否需要扩容。

② 如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。

③ 而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在该位置形成链表。新new的Entry会加到链表的头部。

5、get 方法

public V get(Object key) {if (key == null)return getForNullKey();Entry<K,V> entry = getEntry(key);return null == entry ? null : entry.getValue();
}final Entry<K,V> getEntry(Object key) {int hash = (key == null) ? 0 : hash(key);for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;
}

① 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。

② 判断该位置是否为链表。

③ 不是链表就根据 key、key 的 hashcode 是否相等来返回值。

④ 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。

⑤ 啥都没取到就直接返回 null 。

6、并发场景下出现死循环

多线程同时put时,如果同时调用了resize操作,可能会导致循环链表产生,进而使得后面get的时候,会死循环。下面详细阐述循环链表如何形成的。

resize函数

数组扩容函数,主要的功能就是创建扩容后的新数组,并且将调用transfer函数将旧数组中的元素迁移到新的数组。

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}//创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];//将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}

transfer函数

transfer逻辑其实也简单,遍历旧数组,将旧数组元素通过头插法的方式,迁移到新数组的对应位置问题出就出在头插法。

void transfer(Entry[] newTable) {//src旧数组
    Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);//由于是链表,所以是个循环过程
        }}
}static int indexFor(int h, int length) {return h & (length-1);
}

下面举个实际例子:

① 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。

② 最上面的是old hash 表,其中的Hash表的size=2, 加载阈值为2∗0.75=1,所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。

③ 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

正常的Rehash的过程:

并发下的Rehash过程:

1)假设我们有两个线程,用红色和浅蓝色标注了一下。

我们再回头看一下transfer代码中的这个细节:

do {Entry<K,V> next = e.next; //假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;
} while (e != null);

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转了。

2)线程一被调度回来执行

先是执行 newTalbe[i] = e;然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)

3)一切安好

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。

4)环形链表出现

e.next = newTable[i] 导致 key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了Infinite Loop。

有人把这个问题报给了Sun,不过Sun不认为这是一个问题。因为HashMap本来就不支持并发,要并发就用ConcurrentHashmap。

这个循环链表问题只存在于JDK1.7中,在JDK1.8中使用了不同的扩容实现方式,所以不会出现这种情况。JDK1.8中HashMap是如何实现的我们后续讲解。


原作者:DIGGKR
原文链接:面试官:说一下HashMap原理,循环链表是如何产生的
原出处:掘客DIGGKR
侵删

面试官:说一下HashMap原理,循环链表是如何产生的相关推荐

  1. hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵

    这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...

  2. 我向面试官讲解了hashmap底层原理,他对我竖起了大拇指

    前言: 正值金九银十的黄金招聘期,大家都准备好了吗?HashMap是程序员面试必问的一个知识点,其内部的基本实现原理是每一位面试者都应该掌握的,只有真正地掌握了 HashMap的内部实现原理,面对面试 ...

  3. 实战系列-被面试官问到Feign原理

    导语   事情是这样的,昨天参加了某公司二面,被面试官问道了Spring Cloud的RESTFul远程调用.项目上用到的技术就是OpenFeign,面试官可能自己不是太了解,给他解释一番发现自己还有 ...

  4. 手写Vuex核心原理,再也不怕面试官问我Vuex原理

    手写Vuex核心原理 文章目录 手写Vuex核心原理 一.核心原理 二.基本准备工作 三.剖析Vuex本质 四.分析Vue.use 五.完善install方法 六.实现Vuex的state 七.实现g ...

  5. 《吊打面试官》系列-HashMap

    你知道的越多,你不知道的越多 敖丙--吊打面试官系列 前言 作为一个在互联网公司面一次拿一次Offer的面霸,打败了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚(请允许我使用一下夸张 ...

  6. 稀疏多项式的运算用链表_用最简单的大白话聊一聊面试必问的HashMap原理和部分源码解析...

    HashMap在面试中经常会被问到,一定会问到它的存储结构和实现原理,甚至可能还会问到一些源码 今天就来看一下HashMap 首先得看一下HashMap的存储结构和底层实现原理 如上图所示,HashM ...

  7. hashmap 遍历_别慌,送你21 个面试官必问HashMap考点

    Java面试笔试面经.Java技术每天学习一点 Java面试 关注不迷路 作者:菜鸟小于 来源:https://www.cnblogs.com/Young111/p/11519952.html 1:H ...

  8. eui加载时间长_面试官:为什么 HashMap 的加载因子是0.75?

    有很多东西之前在学的时候没怎么注意,笔者也是在重温HashMap的时候发现有很多可以去细究的问题,最终是会回归于数学的,如HashMap的加载因子为什么是0.75? 本文主要对以下内容进行介绍: 为什 ...

  9. 面试官:为什么 HashMap 的加载因子是0.75?

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"加群",加入新技术 来源:8rr.co/8V9Q 有很多东西之前在学的时候没怎么注意, ...

  10. 被替换的项目不是替换值长度的倍数_面试官,为啥HashMap的长度是2的n次方?

    前言 HashMap的主干是一个数组,假设我们有3个键值对dnf:1,cf:2,lol:3,每次放的时候会根据hash函数来确定这个键值对应该放在数组的哪个位置,即index = hash(key) ...

最新文章

  1. Netty傻瓜教程(一):Netty初探,只写个服务端也能工作
  2. C++ STL之vector常用方法
  3. CentOS 7 NAT软路由
  4. 优先队列 HDOJ 5437 Alisha's Party
  5. python global用法_【干货】每天更新两个Python 小例子(十九)
  6. linux如何查看nginx是否启动
  7. html新标准,HTML 5新标准将会在2022年正式发布
  8. python 分类变量xgboost_XGBoost的介绍、应用、调参、知识点
  9. 立春----直流电压电流检测模块
  10. oracle 根据出生日期计算年龄
  11. 中国经典营销案例—农夫山泉
  12. html表格标题标签_HTML标题标签
  13. Windows 下定制黑苹果 USB 驱动教程
  14. 计算机毕业设计抄袭,学生毕业设计抄袭他人纪录片,只算“侵权”? 西安工程大学称属学术不端...
  15. OracleORA错误解决方案
  16. 基于机器学习的恶意软件加密流量检测研究分享
  17. 支付宝手机网站支付开发记录之结果异步通知
  18. EOS系列 - 解决升级EOS2.0 `env.set_proposed_producers_ex unresolveable` 问题
  19. 人的记忆组成图(原创整理,转载请注明)
  20. [译]在HealthKit中用 Swift 进行睡眠分析

热门文章

  1. Linux学习|什么是GPL(General Public License,GNU通用公共许可协议?
  2. 三菱变频器E700系列和FX3U系列485通讯
  3. 小孩子爱玩手机学计算机编程好吗,孩子爱玩手机/电脑就是有网瘾?看完这篇文章你就不会太焦虑了...
  4. HTML5智慧渔业WebGL可视化云平台
  5. 什么是ROM-BIOS
  6. 2021物联网五大发展趋势
  7. 如何愉快的填写问卷星
  8. 程序员年入百万指南(二)之为什么程序员应该懂点销售
  9. 计算机游戏自动化测试软件,Airtest IDE
  10. 小拌同学麻辣烫•麻辣拌,开启学生主题美食校园经济的钻石商机!