美文网首页
HashMap — JDK1.8

HashMap — JDK1.8

作者: 凌晨的咸鱼 | 来源:发表于2021-02-02 18:07 被阅读0次

第一步:new一个HashMap

HashMap<String, Object> map = new HashMap<>();

源码分析如下:

涉及到的成员变量:
final float loadFactor;  // 使用的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 默认的加载因子

构造函数就一行,使用默认的加载因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;  // 使用默认的加载因子数值
    // 记得jdk1.8之前构造函数会指定初始容量的,这里并没有,说明在jdk1.8版本中取消了 
}

第二步:调用put()方法添加一个元素

map.put("name", "xianyu");

源码分析如下:
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);  
}

一共两个方法:里层的hash(key)方法和外层的putVal(......)方法

- hash(key)方法:计算map中key的hash值,源码自行debug跟进,其中需要注意的是:key的hash值是循环key值的每一个字符进行
运算,比如xianyu是6个字符就循环6次进行运算,xianyuxianyu就是循环12次进行运算,所以尽量不要太长,循环次数太多对效率肯定
没有那么友好。
- putVal(hash(key), key, value, false, true)方法:存储map数据过程的方法

涉及到的成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  // map的初始默认容量,数值为16
static final float DEFAULT_LOAD_FACTOR = 0.75f;  // map的默认加载因子
int threshold;  // 扩容的阀值,容量达到阀值数之后就要对map进行扩容,其值等于map的容量乘以加载因子
transient Node<K,V>[] table;  // 我们都知道map中维护的是Note数组,其实就是这里面的table数组中的数据

先来看一下Note类源码

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        // 是不是很简单,这个类就4个变量,key的hash值,key值,value值,下一个Note
        // 所以Node其实就是一个单链结构,里面存储的才是实实在在真正的数据
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        // 省略一大堆不需要关心的代码......
    }

当我们第一次调用putVal(...)方法时,代码会省略没有调用的部分

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // table定义见上方,默认为null赋给tab,所以tab为null,if判断为true进入
        if ((tab = table) == null || (n = tab.length) == 0)
            // 调用resize()方法给tab数组赋值,然后调用tab数组的长度赋值给n
            // resize()方法的作用是对map的Note数组进行初始化并作为返回值赋值给tab,具体实现见下方
            n = (tab = resize()).length;

        // 调用 (n - 1) & hash 的结果赋值给i,然后把tab数组中下标为i的Note赋值给p并判断是否为null
        // (n - 1) & hash 为Note数组长度n和hash值取余的高效率算法
        // 此时tab数组中一个元素都没有,所以不管取第几个下标数据肯定都为null,if判断为true进入
        if ((p = tab[i = (n - 1) & hash]) == null)
            // Note数组中下标为i的Note进行赋值,所以如果tab中取的那个Note为null,就根据数组长度n和hash取余的结果作为此次数组下标进行赋值
            tab[i] = newNode(hash, key, value, null);

        // 这里省略第一次调用putVal()方法时不会执行的代码一大堆......

        ++modCount;  // 统计次数

        // size值加一,并判断,如果size值大于阀值,就需要resize扩容去了 
        // 首次默认容量为16,默认加载因子为0.75,所以阀值threshold为12,所以当第12次添加元素的时候,++size值为13,
        // 13大于12就需要扩容了
        if (++size > threshold)
            resize();

        // 此行代码的实现未做任何处理,是给实现了HashMap的LinkedHashMap去实现使用的
        afterNodeInsertion(evict);

        // 第一次向map里添加元素就这么简单结束里,添加成功返回值为null 
        return null;
    }

总结:resize()方法对table数组进行初始化,然后根据容量和hash值进行取余得到下标,第一个元素就放在table对应的下标位置

第一次调用resize()方法时,代码会省略没有调用的部分

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;  // table为null,oldTab也为null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  // oldCap值为0
        int oldThr = threshold;  // 默认都等于0
        int newCap, newThr = 0;
        if (oldCap > 0) {  // oldCap等于0,不成立,不进入
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        }
        else if (oldThr > 0) //  // oldThr等于0,不成立,不进入
            newCap = oldThr;
        else {               // 上面的都不成立,所以进入else
            // 默认容量16赋值给newCap
            newCap = DEFAULT_INITIAL_CAPACITY;  
            // 默认容量16 乘以 默认加载因子0.75,等于12赋给newThr
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

        // newThr值为12赋给map的阀值,说明第一次添加元素后确定了阀值为12,只要map的键值对不超过12就不会扩容
        threshold = newThr;  

        @SuppressWarnings({"rawtypes","unchecked"})
        // 此处关键,创建一个长度为newCap的Note数组,因为前面newCap为默认容量16,所以此处就创建了一个容量为16的Note
        // 数组,然后赋值给table,前面我们知道,table就是map里面真正的数据,所以,这2行代码其实就是给首次给table赋值
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

        if (oldTab != null) {  // oldTab为null,跳过
            // 此处省略一大堆跳过的代码......
        }
        return newTab; 
    }

总结:首次执行resize()方法很简单,可以看出,最主要就2个功能,根据默认的容量和加载因子计算出阀值赋给threshold,创建一个容量为16的Note数组赋给table,之后,就可以对这个Note数组进行赋值操作了

其他记录:

扩容操作:

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
这段代码是扩容的操作代码,扩容时,newCap之前的容量 和 newThr之前的阀值 都左移一位等于乘以2,所以每次扩容时,容量和阀值大
小都会变为以前的2倍
扩容时,会重新创建一个容量为新容量的Note数组,然后把之前的旧Note数组的数据一个一个拷贝进去,所以,如果map中要存大量数据,
尽量在初始化的时候指定容量大小,不然频繁的扩容导致频繁的拷贝影响性能

下面扩容代码操作:
      if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 如果此Node的next为null,即未形成链表,进入if
                    if (e.next == null)  
                        // 此Node值e在新数组的下标位置为 自身hash值和新数组容量的取余结果
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                        // 这里为形成链表的Node扩容过程,赋值到新数组中还是链表结构
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //根据e.hash & oldCap 判断节点存放位置
                            //如果为0 扩容还在原来位置 如果为1 新的位置为 旧的index + oldCap
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
map中的key来决定添加的数据在数组中的位置:

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
这段代码说明数据在map数组中的位置不是由key的hash值决定的,而是由阀值减1和hash值的取余算法的结果决定的
形成链表 和 链表转为红黑树 的代码:

        // 如果此时tab中第i个Node不为空则进入else
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // hash值不相同,key也不相同,所以不进入此if
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // p不属于TreeNode红黑树类型,不进入此 else if
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 进入这里的代码
                for (int binCount = 0; ; ++binCount) {
                    // 因为还没有链表形成,所以Node的next是空的
                    if ((e = p.next) == null) {

                        // 关键代码:形成链表了,Node中的next赋值为了新添加的Node数据
                        p.next = newNode(hash, key, value, null);
              
                        // static final int TREEIFY_THRESHOLD = 8;  // 固定值为8
                        // 关键代码:达到TREEIFY_THRESHOLD的值链表即转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            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;
            }
        }


备注:

HashMap在jdk1.8之前采用数组加链表,多线程环境下,会出现死循环,是因为在执行resize扩容操作时,形成链表的时候
采用的是头插法。在jdk1.8之后,采用数组加链表和红黑树结构,采用尾插法,解决了死循环的问题。但是因为没有任何的
同步操作,在多线程put数据的时候依然可能会出现丢失数据等情况。
所以,在多线程环境下,建议使用ConcorrentHashMap,ConcorrentHashMap在put元素和扩容的时候都使用了Synchronized来同步操作

扩展问题:

HashMap<StringBuffer, Object> map = new HashMap<>();

        StringBuffer a = new StringBuffer("a");
        StringBuffer b = new StringBuffer("a");

        map.put(a, "xianyu");
        map.put(b, "xianyu");

        System.out.println(map);
        System.out.println(map.size());

        请问:打印出来的map是什么,map.size()等于几?如果你答错的话可以一起交流哦!

相关文章

  • HashMap 底层是怎么样的

    JDK1.8 之前 JDK1.8 前,HashMap 底层是 数组+链表,也就是 链表散列。 HashMap 通过...

  • 手写简单HashMap

    今日学习:1、了解jdk1.8版的HashMap原理2、手写jdk1.8版之前的HashMap 前言     好几...

  • JDK1.8的HashMap源码分析

    JDK1.8之前的HashMap 在JDK1.8之前,HashMap通过散列表(哈希表)实现,并且散列表冲突解决方...

  • Java集合目录

    一、简述 二、原理分析 HashMap(JDK1.7) HashMap(JDK1.8)

  • HashMap源码解析

    HashMap在JDK1.8之前底层的实现方式是数组+链表,从JDK1.8开始对HashMap底层进行了优化,改为...

  • 数据结构解析-HashMap

    概要 HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优化,...

  • 数据结构解析-HashMap

    概要 HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优化,...

  • 深入浅出学Java-HashMap

    一、概要 HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优...

  • HashMap

    1.概要 HashMap在JDK1.8之前的实现方式数组+链表,但是在JDK1.8后对HashMap进行了底层优化...

  • HashMap源码分析(JDK1.8)

    HashMap简介 JangGwa从源码角度带你熟悉一下JDK1.8的HashMap,首先简单介绍下HashMap...

网友评论

      本文标题:HashMap — JDK1.8

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