带你走进Java集合_HashMap源码分析1

前几篇博客主要从源码角度分析了List集合的两个重要的实现类ArrayList、LinkedList,今天我们先跳过Set集合,直接讲解Map的主要实现类,因为Set集合的主要实现类HashSet、TreeSet底层主要用Map的实现类,所以我们先分析Map,然后回过头来看Set就非常的简单了。所有的Map集合JDK7和JDK8以后源码实现差别非常的大,我们主要以JDK8的源码分析。本篇文章主要讲解HashMap,学习HashMap主要学习它的数据结构。因为HashMap内容较多,我们会用几篇文章去介绍HashMap.

一、HashMap的底层数据结构

在JDK8以后,HashMap底层数据结构编程了数组+链表+红黑树

通过获得key的hash值可以获取被插入的值在数组的下标,如果key的hash冲突,那就会在这个下标形成链表,但是链表的时间复杂度为o(n),当冲突多时,链表很长,导致查询效率下降,所以为了防止效率下降,如果链表的长度大于8时,链表就会变成红黑树,而红黑树的时间复杂度是o(logn)

《带你走进Java集合_HashMap源码分析1》

二、HashMap的重要属性

1)DEFAULT_INITIAL_CAPACITY 默认的初始化大小

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

注意HashMap的容器一定是2的整数次幂,原因我们接下来会详细阐述。

2)MAXIMUM_CAPACITY最大容器

static final int MAXIMUM_CAPACITY = 1 << 30;

3)DEFAULT_LOAD_FACTOR默认加载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

4)loadFactor 加载因子

final float loadFactor;

这个属性,刚接触HashMap源码的同学搞不懂是干什么的,我们上面说过HashMap的底层是数组,但是数组的长度是不变的,所以达到某个临界点时就需要扩容,而这个加载因子就是这个临界点,例如一个HashMap的容器大小为16,如果HashMap中的元素超过12=16*0.75,时就需要对容器扩容了,所以加载因子与HashMap的扩容有关,只要size大于容量的0.75倍就需要进行扩容。

4)threshold,我们可以认为他是扩容的阀门。

int threshold;

这个属性和加载因子一样,与扩容有关,它的值等于当前容器大小*加载因子,我们上面计算的12=16*0.75,12就是计算出来的threshold的值,就是当元素大小超过12就需要扩容。

上面loadFactor和threshold两个元素与扩容有关,loadFactor是加载因子,默认为0.75,loadFactor的值可以大于1,但是综合空间和时间的考虑还是使用默认的加载因子DEFAULT_LOAD_FACTOR=0.75。而阀门threshold的值与加载因子有关。

5)TREEIFY_THRESHOLD 这个值是从链表变成红黑树的阀门,如果大于这个值就会转变

static final int TREEIFY_THRESHOLD = 8;

6)table 底层数组,就是HashMap的底层数组

transient Node<K,V>[] table;

因为数组的查询的时间复杂度是o(1),

(1)如果没有hash冲突,则所有的数据会放到这个table数组中,

(2)如果有hash冲突且链表的长度小于等于8,则会把链表的第一个节点放到table中.

(3)如果有hash冲突且是红黑树,则红黑树的根节点会放到table中

7)size  HashMap实际存放元素的个数

transient int size

我们先记住这些非常重要的属性,其他的属性下面用到的都会讲到,我们接下来说一下Node,在链表转换成红黑树前,我们的元素都会封装成一个Node数据结构,源码如下:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
 }

(1)hash:表示key的hash

(2)key:表示我们给出的key,就是map(key,value)中的key

(3)value:表示我们给出的value,就是map(key,value)中的value

(4)next:如果hash相同,会形成链表,当前链表节点的下一个链表的引用,即链表后继。

如果链表的长度大于8后,为了查询的性能,会把链表转换成红黑树TreeNode。

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
 }

TreeNode的源码较长,主要是为红黑树服务的,我们接下来会有一篇文章介绍红黑树,这里我们主要阐述TreeNode的属性。

(1)parent:表示节点的父节点

(2)left:表示左节点

(3)right:表示右节点

(4)prev:表示前驱节点

(5)red:表示是红树还是黑树

三、HashMap的构造函数

在讲解HashMap的构造函数之前,我们要记住一个非常重要的知识点:HashMap是懒加载的,他在构造函数中并没有对底层数组进行初始化,数组初始化的工作在第一次调用put时初始化的,构造函数主要是对一些属性进行了赋值。

1)第一个构造函数:无参构造函数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以看出它并没有对数组进行初始化,而是将加载因子初始化成默认的加载因子0.75

2)第二个构造函数

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

这个构造函数调用了下面的构造函数

3)第三个构造函数

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);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

(1)首先判断给出的初始化容量initialCapacity是否合法,判断给出的加载因子是否合法,我们上面说了,默认的加载因子0.75是基于空间和时间的综合考虑,一般使用默认的加载因子即可

(2)初始化加载因子

(3)上面我们说了,容器的大小必须是2的倍数(原因我们后面会分析),但是用户给出的可能不是2的倍数,所以HashMap源码要对用户给出的容器大小变成2的倍数,并计算出阀门的大小。

HashMap的源码怎样把用户给出的初始化容量大小变成2的倍数的呢?让我们分析tableSizeFor来揭开其中的面纱。

static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

该方法用来返回大于或者等于输入参数最接近的2的整数次幂的值。例如:

举例1:我们输入的cap=7,那么大于或者等于输入参数最接近的2的整数次幂的值8

举例2:我们输入的cap=8,那么大于或者等于输入参数最接近的2的整数次幂的值8

举例3:我们输入的cap=9,那么大于或者等于输入参数最接近的2的整数次幂的值16

举例4:我们输入的cap=15,大于或者等于输入参数最接近的2的整数次幂的值16

1)第一行代码int n=cap-1,为什么减1呢?这是因为如果不减1,如果给出的cap是2的整数次幂,例如cap=8,通过下面的多次运算,结果变成了16,这就违背了最接近的2的整数次幂。而通过cap-1,最终算出来的是8。所以要进行cap-1.

2)我们要知道一个知识点:或运算,记住这一点:遇1则1

我们举例说明:cap=7

(1)int n=cap-1 计算结果 n=6   二进制表示:0110

(2)n|=n>>>1  

《带你走进Java集合_HashMap源码分析1》

 (2)n|=n>>>2

《带你走进Java集合_HashMap源码分析1》

   (3)n|=n>>>4

《带你走进Java集合_HashMap源码分析1》              

(4)n|=n>>>8

《带你走进Java集合_HashMap源码分析1》

(5)n|=n>>>16

《带你走进Java集合_HashMap源码分析1》

通过几步的与运算,此时n=7,

(6)最后一句代码则return 8,即返回大于或等于最接近输入参数的2的整数次幂

所以这个方法非常的巧妙的把用户给的容器的大小变成了2的整数次幂并返回

这个方法要注意一下知识点:

1)为了防止给出的cap本来就是2的整数次幂,所以先进行减1处理,防止给出的是8,到最后的计算后获取的是16。

2)通过几次与运算后,从给出的最高位1开始的位数都是1,例如:1000,通过与运算变成了0111,

3)这个方法不是初始化底层数组的大小的,初始化数组的动作在第一次put的时候,这个方法计算出来的值初始化给了threshold,但是我们前面不是讲了,threshold=tableSizeFor(initialCapacity)*laodFactor吗?,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

通过我们上面的讲解,是不是对HashMap的构造函数有了非常清晰的认识,我们总结一下

1)HashMap的构造函数并没有对底层的数组进行初始化,而是放到了第一次调用put的时候,构造函数只是

初始化了加载因子loadFactor和阀门threshold

2)如果用户指定了容器的大小,HashMap只是把给定的initCap通过调用tableSizeFor方法,把给定的参数变成大于或者等于最接近输入参数的2的整数次幂,并初始化给阀门threshold

本篇文章就先写到这里,我们上面有一个问题还没有解决,为什么需要变成2的整数次幂呢?下一篇文章我将给大家分析其中的原因。总结一下这一篇稳重的知识点:

1)HashMap的底层数据结构是数组+链表+红黑树

2)加载因子loadFactor和阀门threshold是为扩容服务的。

3)底层数组的容量大小一定是2的整数次幂(后文解释)

4)HashMap构造函数并没有初始化数组,初始化数组的动作在第一次调用put的时候进行的。

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/shaotianqiang/article/details/80712782
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞