美文网首页
基于jdk1.8的HashMap源码分析(一)

基于jdk1.8的HashMap源码分析(一)

作者: 小甲说 | 来源:发表于2018-10-10 22:58 被阅读0次

    1.概念

        HashMap是Java中最常用的集合类之一,以<K,V>的形式来进行数据存储。常用来比较的还有HashTable,ConcurrentHashMap 等。

    2.数据结构   

            什么是数据结构,在我的认识中,我们的数据需要存储在计算机中,那么它的存储形式,就是一个个的数据结构。常见的数据结构包括:数组,链表,二叉树,红黑树等等

            提起hashMap内部的数据结构 那么大多数开发者都可以说出 在其内部是维护了一个数组,数组的每个元素又是一个单向链表。在1.7之前的版本中,这是没错的,但是维护单向链表由此带来的一个缺点就是当我们的hashMap发生大量的指针碰撞之后,当我们需要get其中的一个值的时候,就有可能需要遍历单向链表,那么时间复杂度就是O(n),为解决这一问题,在1.7之后,hashMap中引入了红黑树,将达到一定条件的链表转变为一个红黑树,此时时间复杂度就变为了O(logn)。

    问,为何不使用比如线性探测法 往数组的其他位置放 如果长度不够 则进行扩容

    答:第一,这样有可能导致 本来数组还有空余位置就触发了扩容,内存浪费。第二,扩容时 需要根据hash值和数组长度重新计算每一个key的hash值,然后重新排列整个map,消耗系统资源。总体上来说,是一个时间换空间的操作。

    3.hashMap如何存储数据

        接下来就是本文的重点了,hashMap到底是如何存储数据的呢?

        让我们带着几个问题来看代码

        (1)数组是什么时候进行初始化的

        (2)数组在内存中为一块连续的内存地址,所以要求我们在初始化数组是设定数组的长度。那么数组的大小设置为多少呢

        (3)hash值如何计算

        (4)什么是hash冲突,发生后冲突后hashMap是如何处理的

    public V put(K key,V value) {

    return putVal(hash(key), key, value,false,true);

    }

    这就是在hashMap中进行put的方法,我们需要关注两个点 第一,putVal(hash(key), key, value,false,true) 这是进行存储操作的方法;第二,hash(key)这是计算hash值的方法 。

    我们先来看第一个

    transient Node[]table;

    Node[] tab; Node p;int n, I;     

    //声明了一个Node对象,Node元素中包含了如下变量

    final int hash;

    final K key;

    V value;

    Nodenext;  //下一个元素的地址引用 佐证了这是一个单向链表

    if ((tab =table) ==null || (n = tab.length) ==0)    //此处为判断声明的Node[]是否为空

    n = (tab = resize()).length;    //为空执行resize() 方法

    在resize()方法中 ,当判断oldTab.length==0时 有如下代码

    newCap=DEFAULT_INITIAL_CAPACITY;

    newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY);

    //下面两行是类中声明的常量

    static final int DEFAULT_INITIAL_CAPACITY =1 <<4;// aka 16  位运算 即初始化的数组长度

    static final float DEFAULT_LOAD_FACTOR =0.75f;// 负载因子

    threshold = newThr;

    @SuppressWarnings({"rawtypes","unchecked"})

    Node[] newTab = (Node[])new Node[newCap];

    table = newTab;

    我们可以看到,当首次进行put时,进行了数组的初始化,并定义了数组的长度为16,那么问题来了,数组的长度给定了之后,tab[]下标为0-15。如果我们不断插入数据,当key的hash值不在这16个长度之内时会发生什么呢,让我们继续看源码,还是putval()

    if ((p = tab[i = (n -1) & hash]) ==null)

    tab[i] = newNode(hash, key, value,null);

    通过这两行代码我们可以看到 去计算了一次hash值 当tab[i]==null时,new了Node元素 放了进去 完成了数据存储

    如果此时tab[i]不为null则执行下面的代码。

    else {

    Node e;K k;

    if (p.hash == hash &&

    ((k = p.key) == key || (key !=null && key.equals(k))))

    e = p;

    在这我们看到  当 k的hash值相等且 equals()相等的时候,进行了覆盖

    else if (pinstanceof TreeNode)

    e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

    k不相等的时候 如果当前Node对象包含了红黑树 那么就把它放在了树里

    else {

    for (int binCount =0; ; ++binCount) {

    if ((e = p.next) ==null) {

    p.next = newNode(hash, key, value,null);

    if (binCount >=TREEIFY_THRESHOLD -1)// -1 for 1st 

    TREEIFY_THRESHOLD =8

                    treeifyBin(tab, hash);

    break;

    }

    对单向链表进行遍历。当p.next==null 是说明这是单向链表的尾结点,指针null。记录单向链表的遍历的次数。 TREEIFY_THRESHOLD=8 当链表长度》=8-1时, 将链表转为红黑树

    if (e.hash == hash &&

    ((k = e.key) == key || (key !=null && key.equals(k))))

    break;

    //同样是进行key的hash判断 一致则覆盖掉原有值

    p = e;

    }

    }

    if (e !=null) {// existing mapping for key

        V oldValue = e.value;

    if (!onlyIfAbsent || oldValue ==null)

    e.value = value;

    afterNodeAccess(e);

    return oldValue;

    }

    //覆盖的过程

    ++modCount;    //记录put的次数

    if (++size >threshold)

    resize();

    //重点来了 在此时,如果数组需要进行扩容数组在什么时候扩容呢,就是代码中的threshold = newThr

    在resize()方法中 如果原有tab长度>0且<2的30次方,则tab长度变为原来的<1 即原来的2倍

    size这个值在哪计算还没发现 待我稍后进行补充

    afterNodeInsertion(evict);

    return null;

    4.完整的源码

    final V putVal(int hash,K key,V value,boolean onlyIfAbsent,

    boolean evict) {

    Node[] tab; Node p;int n, i;

    if ((tab =table) ==null || (n = tab.length) ==0)

    n = (tab = resize()).length;

    if ((p = tab[i = (n -1) & hash]) ==null)

    tab[i] = newNode(hash, key, value,null);

    else {

    Node e;K k;

    if (p.hash == hash &&

    ((k = p.key) == key || (key !=null && key.equals(k))))

    e = p;

    else if (pinstanceof TreeNode)

    e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

    else {

    for (int binCount =0; ; ++binCount) {

    if ((e = p.next) ==null) {

    p.next = newNode(hash, key, value,null);

    if (binCount >=TREEIFY_THRESHOLD -1)// -1 for 1st

                            treeifyBin(tab, hash);

    break;

    }

    if (e.hash == hash &&

    ((k = e.key) == key || (key !=null && key.equals(k))))

    break;

    p = e;

    }

    }

    if (e !=null) {// existing mapping for key

                V oldValue = e.value;

    if (!onlyIfAbsent || oldValue ==null)

    e.value = value;

    afterNodeAccess(e);

    return oldValue;

    }

    }

    ++modCount;

    if (++size >threshold)

    resize();

    afterNodeInsertion(evict);

    return null;

    }

    本篇解决了1.2.4三个问题 至于第3个问题 改日在叙。如有谬误如疏漏之处烦请各位看官多多指正

    相关文章

      网友评论

          本文标题:基于jdk1.8的HashMap源码分析(一)

          本文链接:https://www.haomeiwen.com/subject/sbimaftx.html