HashMap的数据结构

简述:

        HashMap是双列集合Map接口下的一个实现类,以KV键值对映射的形式存储数据

        特点:key唯一,value允许重复,无序,“按键得值”,在HashMap中key和value都允许为null。

数据结构

HashMap中用于存放数据的是一个Node类型的数组【Node<K,V>[] table

jdk1.8之前:数组+单向链表

        在put元素时计算hash值是通过哈希值%数组长度

jdk1.8:数组+单向链表+红黑树

        在put元素时计算hash值是通过(数组长度【此处的数组长度必须为2^n】-1)&哈希值

下面我们重点了解一下HashMap内部的具体存储

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//数组默认初始容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子

static final int TREEIFY_THRESHOLD = 8;//转换为红黑树时链表的阈值

static final int MIN_TREEIFY_CAPACITY = 64;//转换为红黑树时做需要的数组最小容量

final float loadFactor;//加载因子,代表数组的使用率

int threshold;//扩容阈值,数组中实际能存入的最大元素个数

table数组内部是一个个Node类型的对象,此处的Node类是HashMap中的内部类,它是Map接口内Entry接口下的一个实现类,实质上是一个个Entry

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//代表数组中的索引【final修饰不可变】
        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;
        }
        ……
    }

一个键值对封装为Node类型的对象,通过哈希算法计算出在table[]中的索引位置并存入数据

HashMap如何扩容

第一次添加KV键值对,数组如果为空,会扩容为默认容量16

//第一次往map中添加元素,若table[]长度为0,那么会走如下的代码分支:
    if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

//resize()调用如下else分支:
    if (oldCap > 0) {
         ……
    }
    else if (oldThr > 0) {
         ……
    }
    else {               
         newCap = DEFAULT_INITIAL_CAPACITY;//16
         //新的扩容阈值:默认加载因子0.75*16=12
         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

因为下面在往HashMap中添加元素过程中还会存在扩容的现象,我们需要具体了解resize的内部逻辑

  • 数组容量超出最大容量,不扩容
  • 数组容量>0且未超出最大容量,扩容操作【2倍】————例如:当map中节点个数>扩容阈值【加载因子*数组容量】
  • 数组为null或长度为0且扩容阈值未初始化【0】,扩容【16】————即初始化一个空的HashMap,第一次put元素
final Node<K,V>[] resize() {
    	//临时存储table数组
        Node<K,V>[] oldTab = table;
    	//判断数组是否初始化
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	//存储原始扩容阈值
        int oldThr = threshold;
    	//定义新的数组容量,并初始化新的扩容阈值为0
        int newCap, newThr = 0;
  //数组容量>0,接着判断是否超出定义的最大容量,超出了修改扩容阈值为Integer类型的最大值【不扩容】,并返回原table数组
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //未超出【扩容操作】(将数组容量扩容2倍)<最大容量并且原容量>=16,将扩容阈值也变为2倍赋给新阈值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
    	//若【数组为null或长度为0】并且旧扩容阈值>0,用旧阈值初始化新阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
//若【数组为null或长度为0】并且旧扩容阈值为0,将数组容量扩容为16,计算新扩容阈值(默认加载因子【0.75】*数组容量【16】=【12】)
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	//以上条件都不成立,阈值未变化仍为0,根据条件将新阈值设为【新容量*加载因子】或【Integer类型的最大值】
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        ……	……	……
        ……	……	……
        return newTab;
    }

那么在添加过程中如何确定在数组中的位置呢?它会通过下面的hash()

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

HashMap中允许key为null

判断key是否为null,如果key==null,存放数组下标为0处;否则,拿key的hashCode与它的高16位再进行一次异或运算(同为0,异为1)【为了减少哈希冲突】

上述操作时为了减少哈希冲突,那么当出现了哈希冲突,它又将何去何从呢?

这时如果不同的key计算出了相同的值,它会在对应数组的该位置形成一个单向链表,将计算出值相同的数据都放在这一条链表上

下面看一下HashMap中添加KV键值对的方法put()

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab[]为null或长度为0,对tab数组通过resize()方法进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //通过哈希算法【(n - 1) & hash】计算下标位置,但此处的数组长度n必须为2^n
        //若当前下标位置为null,代表没有占用,将数据封装为Node节点存入数组相应的下标中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //当前位置已占用
        else {
            Node<K,V> e; K k;
            //先比较hash,若hash相同再调用equals比较内容,若equals比较也相同,则直接替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //若Node p是一个TreeNode类型,当前链表已是红黑树,将当前节点加入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //当前还是链表
            else {
                for (int binCount = 0; ; ++binCount) {
                    //jdk1.8后采用尾插法,找到尾结点,尾结点——>next=封装的Node节点,且新的尾结点next=null
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //若链表长度>8,调用treeifyBin()【还需进一步判断数组长度才能做出决定是否转换为红黑树】
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //比较hash,hash相同再调用equals比较是否相同,相同则替换
                    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;
        //若map中存放的节点已经达到扩容阈值,则需要对数组进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

链表与红黑树之间的转换

但是不是这条链表是不是能一直延伸下去呢?答案是否

当链表上节点个数>8时,会调用treeifyBin方法

        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        //binCount记录链表上节点的个数,从0开始
                            treeifyBin(tab, hash);

treeifyBin方法具体代码:

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //数组为空或数组长度<64会进行扩容操作
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //数组不为null并且数组长度>=64时才会执行如下操作
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

注意在treeifyBin方法中我们看见并没有直接进行链表转换红黑树的操作,而是先判断了当链表节点个数>8时它的数组长度是否<64,若满足条件,会将数组再次进行扩容,而不会进行转换红黑树的操作,只有当链表节点个数>8且数组长度>64,此时链表会转换为一颗红黑树

小结

  • HashMap内部存储采用数组+链表+红黑树
  • 链表转换为红黑树的条件:参照注意内容
  • 注意扩容时机
    • 1、数组为空,第一次添加元素会扩容为16
    • 2、当HashMap中存在节点数>threshold数组可用的最大容量时,扩容2倍
    • 3、当HashMap链表节点个数>8且数组长度<64,扩容2倍
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值