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[] 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[] table,这个数组,那么它又是如何定义的呢?

static class Entry implements Map.Entry {    final K key;    V value;    Entry next;    int hash;    /**     * Creates new entry.     */    Entry(int h, K k, V v, Entry 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 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 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 entry = getEntry(key);    return null == entry ? null : entry.getValue();}final Entry getEntry(Object key) {    int hash = (key == null) ? 0 : hash(key);    for (Entry 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 e = src[j];        if (e != null) {            src[j] = null;            do {                Entry 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,然后所有的 重新rehash的过程

正常的Rehash的过程:

并发下的Rehash过程:

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

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

do {   Entry 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是如何实现的我们后续讲解。

欢迎小伙伴们留言交流~~

hashcode是什么意思_面试官:说一下HashMap原理,为什么会产生死循环相关推荐

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

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

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

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

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

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

  4. redis查看key的过期时间_面试官:你在Redis中设置过带过期时间的Key吗?

    点击上方小伟后端笔记关注公众号 每天阅读Java干货文章 熟悉Redis的同学应该知道,Redis的每个Key都可以设置一个过期时间,当达到过期时间的时候,这个key就会被自动删除. 在为key设置过 ...

  5. redis list设置过期时间_面试官:你在Redis中设置过带过期时间的Key吗?

    点击上方小伟后端笔记关注公众号 每天阅读Java干货文章 熟悉Redis的同学应该知道,Redis的每个Key都可以设置一个过期时间,当达到过期时间的时候,这个key就会被自动删除. 在为key设置过 ...

  6. 引用计数器法 可达性分析算法_面试官:你说你熟悉jvm?那你讲一下并发的可达性分析...

    持续输出原创文章,点击蓝字关注我吧 上面这张图是我还是北漂的时候,在鼓楼附近的胡同里面拍的. 那天刚刚下完雨,路过这个地方的时候,一瞬间就被这五颜六色的门板和自行车给吸引了,于是拍下了这张图片.看到这 ...

  7. 面试java你最擅长什么_面试官最喜欢问的10道Java面试题

    1.Java的HashMap是如何工作的? HashMap是一个针对数据结构的键值,每个键都会有相应的值,关键是识别这样的值. HashMap 基于 hashing 原理,我们通过 put ()和 g ...

  8. java录排名怎么写_面试官:Java排名靠前的工具类你都用过哪些?

    你知道的越多,不知道的就越多,业余的像一棵小草! 你来,我们一起精进!你不来,我和你的竞争对手一起精进! 编辑:业余草 推荐:https://www.xttblog.com/?p=5158 在Java ...

  9. redis删除过期key的算法_面试官别再问我Redis内存满了该怎么办了

    概述 Redis的文章,我之前写过一篇关于「Redis的缓存的三大问题」,累计阅读也快800了,对于还只有3k左右的粉丝量,能够达到这个阅读量,已经是比较难了. 这说明那篇文章写的还过得去,收到很多人 ...

最新文章

  1. php类实例方法静态方法,PHP类中的静态方法使用实例
  2. centos7安装配置ELK(Elasticsearch+Logstash+Kibana)
  3. 立体匹配算法实现之:AdaptWeight
  4. 美媒:中国大陆最火的工作,教人工智能识图
  5. 计算机考试网络应用题一定要做到ie浏览器,网络远程教育统考单项练习:计算机应用基础之Internet应用部分(二)...
  6. 值传递,引用传递,指针传递
  7. SPARK学习之 --- eclipse / sbt / scala 配置
  8. rsync+inotify 实现数据实时同步
  9. 一文搞懂Java泛型到底是什么东东
  10. 【Ajax】后台验证用户输入的验证码是否与随机生成的验证码一直
  11. 声学计算机软件,常用声学仿真软件汇总
  12. java中的Properties配置文件
  13. C++卡常数之内存优化
  14. AMiner推荐论文:Strongly coupled N-doped graphene quantum dots/Ni(Fe)OxHy electrocatalysts with accelerat
  15. 积分商城系统业务逻辑思维导图_怎么开发积分商城系统_OctShop
  16. 【Linux】虚拟机VMware的Ubuntu使用vi指令的方向键和backspace空格键乱码
  17. LXC的网络结构和端口映射
  18. 国内外php商城系统 开源、php商城比较。
  19. Glide自定义缓存key
  20. Mac M1 nvm install失败问题

热门文章

  1. Oracle 非dba用户 使用 set autotrace 功能
  2. oracle rman 实例,Oracle数据库rman常用命令的使用示例
  3. 计算机应用第7章在线测试,《计算机应用基础》第07章在线测试
  4. java四种修饰符_java中的四种修饰符
  5. 基于JAVA+SpringMVC+Mybatis+MYSQL的田径运动会管理系统
  6. 极递云课显示服务器连接超时,服务器连接问题
  7. 苏州大学计算机组成题库11,苏州大学计算机组成题库(范文).doc
  8. java applet 换行_如何用java applet 画字符串,宽度大于设定值,自动换行
  9. Android BottomNavigationBar导航栏
  10. python的变量与注释