JDK1.8源码逐字逐句带你理解HashMap底层(1)

引言:

自己在学习这个东西的时候,发现网上很多关于HashMap底层介绍的文章基于的jdk版本比较低。因为我对比之后发现编码风格有了比较大的改变。而且,今天我想尝试一种很通俗的方式来尝试记录这次的学习。在本文中我主要整理了HashMap类的重要成员变量和关键方法的涵义和作用,HashMap初始化方式并描述初始化变量。了解HashMap存储结构,根据JDK源码逐字逐句解读核心方法。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

技术点:

1、数组与链表

简单通俗来说,两者各有优劣。对于数组来说,它的存储空间是连续的,占用内存严重,连续的大内存进入老年代的可能性也会变大(关于GC后面我会把学习的也记录下来),但是正因为如此,寻址就显得简单,也就是说查询某个arr会有指定的下标,但是插入和删除比较困难,因为每次插入和删除时,如果数组在插入这个地方后面还有很多数据,那就要后面的数据整体往前或者往后移动。对于链表来说存储空间是不连续的,占用内存比较宽松,它的基本结构是一个节点(node)都会包含下一个节点的信息(如果是双向链表会存在两个信息一个指向上一个一个指向下一个),正因为如此寻址就会变得比较困难,插入和删除就显得容易,链表插入和删除的时候只需要修改节点指向信息就可以了。

2、哈希表/散列表(Hash table 注意这个不是JAVA线程安全类:HashTable)

你有故事我有酒”,很多时候两者结合才显得韵味十足。在哈希表的结构中就融入了数组和链表的结构,从而产生了一种寻址容易,插入删除也容易的新存储结构,下图是百度百科引入的图片:

这里写图片描述

其实哈希表的实现方式有很多种,我们就研究如上这最常用的一种:

为了解释方便,我们定义两个东西:String[] arr; 和 List list;
那么,上图左边那一列就是arr, 就是整个的arr[0]~arr[15],且arr.length() = 16。上图每一行就是一个list,这里理论来说应该最大存储16个List。每个数组存存放的应该是某一个链表的头,也就是arr[0] == list.get(0)。不知道我这样的描述是否清楚。

那么如何确定某一个对象是属于数组的某个下标呢?一般算法就是 下标 = hash(key)%length。算式中的key是存放的对象,hash这个对象会得到一个int值,这个int值就是在上图中所体现的数字,length就是这个数组的长度。我们用上图中的arr[1]打个比方,1%16 =1,337%16 = 1, 353%16 =1。大家就存储在arr[1]中。

HashMap详解

主要成员变量和方法

  • loadFactor:称为装载因子,主要控制空间利用率和冲突。大致记住装载因子越大空间利用率更高,但是冲突可能也会变大,反之则相反。源码中默认0.75f。
   /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • DEFAULT_INITIAL_CAPACITY与MAXIMUM_CAPACITY:称为容量,用于控制HashMap大小的,下面是源码中的解释:

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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;
  • THRESHOLD: 这个字段主要是用于当HashMap的size大于它的时候,需要触发resize()方法进行扩容。下面是源码:
    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

看到这里,也许一部分人心中会有一个疑问,为什么前面赋值要用移位的方式,而这里就直接赋值8而不用1<<3呢?注意一个点,在上面有一句解释:“ MUST be a power of two.”,意思就是说这个变量如果发生变化将会是以2的幂次扩容的。比如说1<<4进行扩容一位的话就是1<<4<1那结果就是32啦。

关于这几个参数的使用和关系请参考下面的HashMap初始化和源码分析。

初始化:

笔者写了一个小Demo:

package com.brickworkers;

import java.util.HashMap;

public class HashMapTest {

    public static void main(String[] args) {

        HashMap<String, String> map = new HashMap<String, String>();
        for(int i = 0; i<1000; i++){
//          map.entrySet();
//          map.keySet();
//          map.values();
            map.put(String.valueOf(i), String.valueOf(i));
        }
    }
}

在逐步debug的过程中,发现new了一个HashMap的时候,map中的初始化情况是这样的:
这里写图片描述

然后等我put一个键值对进入的时候就会变成这样:

这里写图片描述

从这个之间的变化,我们发现在new了一个新的hashMap的时候并没有对所有的成员变量进行赋值。当触发了put操作之后就开始变化了。
接着,我们点开table,table主要是键值对数组,也就是存在HashMap中真真实实存的值:
这里写图片描述

发现table是一个长度为16的数组。在这里我们总结一下。table.length为16, threshold:12,loadFactor:0.75。是不是发现table.lenth = threshold/loadFactor呢?前面有说道capacity这个变量,其实这个变量就是table.length,但是大家要区分好capacity和size的区别。上图中的size是HashMap中已存在存储对象的数量。

接着,我们继续研究,前面提到,触发扩容(resize()方法)是数据的size>rhreshold的时候,那么我们就debug到扩容阶段:

触发扩容

扩容成功,发现对应的threshold变成了24,table.length = 32。我们验证一下前面的算式对不对:24/0.75 =32。还有一个有趣的一点,table中的数组存放顺序是这样的(仔细看上图应该也能看出):[11=11, 12=12, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, null, null, null, null, null, 10=10],是不是觉得很有意思?

有的小伙伴又有疑问了,那么那个一直在变化的modCount又是什么东西呢?这个东西其实是渗透在HashMap类中的方法里面的,只要HashMap发生一次变化,就会对应的+1,可以称为修改次数计数器。它主要是存在与非线程安全的集合当中。我们知道HashMap是一种非线程安全的类(这里涉及到一个HashMap与HashTable的区别),那么如果某一个HashMap对象你在操作的过程中,被别的线程修该了怎么办?那不是乱套了么?因此,modCount就应运而生啦,在迭代器初始化过程中会将modCount值赋的给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map,从而抛出ConcurrentModificationException。这就是fail-fast策略。不知道笔者这样描述大家能否理解呐?

对于那些看到这里还兴趣盎然的小伙伴们说一声道歉,笔者本来打算今天把所有的一次性都写完,但是还有一些事情和工作需要去完成,所以呀,写到这里我上去给标题最后面加了个(1)。关于HashMap的逐字逐句介绍源码底层实现和一些关键方法put(),get(),resize()等等只有下一篇博文再把自己学到的一些皮毛展示给大家哦。

下面是我的微信二维码,加个好友没事可以聊聊天:
这里写图片描述

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值