新手学源码__HashMap底层实现

前言

终于来到了HashMap咯!给我的感觉就是面试必备啊- -源码面前,了无秘密

HashMap

早就听闻HashMap牛逼~快速存取,那么它是如何做到的呢?让我们一步一步地去揭开它的面纱~它不是线程安全的!有modCount了~没错又是它~

维护的属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,默认容量初始16就是有16个桶
static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量1的30次方。**注意**!!!容量必须是2的倍数!之后的内容会告诉你为什么
static final float DEFAULT_LOAD_FACTOR = 0.75f;//装载因子0.75就是容量达到0.75的时候自动扩容
static final int TREEIFY_THRESHOLD = 8;// 转换成树结构的阀值8,就是说,桶的个数超过8的时候进行转换
static final int MIN_TREEIFY_CAPACITY = 64;// 当table容量超过64的时候,就是有不止64个键值对的时候,才能转换成树
transient Node<K,V>[] table;//存放结点的数组,每一个index就是一个桶,这一点概念很重要!
transient int size;// 键值对的数量
int threshold;// 阀值,作用是hashMap容量达到它的时候扩容

所用的数据结构

数组+链表+红黑树

构造函数

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

默认构造函数里面,只指定了装载因子,其它都暂时是null这对接下来resize的几个if语句有作用,勿忘!

主要起作用的就是它HashMap(int, float)

    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);
    }

精髓之处

    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;
    }

当时看到这个小算法的时候吓我一跳0.0,>>> 无符号右移,第一次看见,这个算法的作用就是得到最接近这个cap的一个2的倍数,比如5穿进去出来就是8,厉害了我的哥

put–键值对存入

大概的步骤
对key的hashCode()做hash,然后再计算index;
如果没碰撞(就是算出来的index不一样)直接放到bucket里;
如果碰撞了,以链表的形式存在buckets后;
如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
如果节点已经存在就替换old value(保证key的唯一性)
如果bucket满了(超过load factor*current capacity),就要resize。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

第一件事情做hash(运算)

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // p用来保存桶的头结点的
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果hash表为空,调用resize完成初始化,n代表长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果要插入的元素没有元素,新建一个节点放进去,i=(n-1)&hash算出index索引,此时p指向插入元素的那个桶的头结点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 如果p头结点的key和要插入的节点key一样,则替换,e用来保存被替换的那个节点的
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 不一样的情况下,如果是树结构,就用数插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 链表的遍历并且插入,binCount记当前桶的结点个数
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果当前桶的节点个数大于8,就要树型化(只是单个桶的树型化)
                        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;
                }
            }
            // 存在e,也就是说有结点被替换出来了,返回被替换的结点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 检查 在没有替换而是新加入的情况下,超过了阀值,则resize,扩容,每次扩容是原来的两倍
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

关键来了!优化之一
我们都知道,当来了一个新的键值对的时候,我们要选择存放的位置,i = (n - 1) & hash便是这个位置的算法。朴素点说就是,一共那么多个桶,往里面按顺序放入东西一个一个来,我们第一个想到的就是取模运算,便能找到位置,但是这个&便是一个高效的算法。hash函数中右移16位的作用就在于减少碰撞,举例子这里写图片描述

解释一下为何要这么做,hashcode得到的结果那么长,单纯用它和n-1做运算,实际用到的就最后那么小几位,而这小几位一样的数很多的,这里的目的就是避免了它,然而还是有一些问题,这个比较复杂我们暂且放一放


resize扩容和初始化

    final Node<K,V>[] resize() {
    // oldTab 指向原来的table
        Node<K,V>[] oldTab = table;
        // 保存旧table的个数和阀值,如果是new HashMap,则这里的table是null!!!也没有threshld 也=0;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        // 新容量和阀值
        int newCap, newThr = 0;
        // table>0表明已经添加了元素
        if (oldCap > 0) {

            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 新容量为原来的2倍后小于最大容量,并且旧容量比16大
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 扩容成原来的2倍
                newThr = oldThr << 1; // double threshold
        }
        // 如果旧容量为0并且旧阀值>0,说明已经创建了hash表,但是没有添加元素
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 什么都没有,阀值和表都没,比如默认构造函数的时候
        else {               // zero initial threshold signifies using defaults
            // 容量为16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新阀值 12
            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);
        }
        // 更新阀值
        threshold = newThr;
        // 旧表的元素移动到新的表里面
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // 旧的桶置位空,因为table里面每一项就代表一个桶
                    oldTab[j] = null;
                    // 如果当前桶只有一个元素,直接复制到新表中对应的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                        // 如果旧表已经采用树结构,新表也用树结构
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //loHead代表老位置的头节点,loTail代表老结点的尾部,hiHead代表新增加的的头部,hiTail代表新增加的尾部,换句话说,一个头指针一个尾指针
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 还记得吗?(n-1)&的时候是把hash值的位数降了1位,现在不减1,用来判断高位是否为1,还是0从来决定新的位置,一会用图来解答
                            // 如果是0还放在老位置,一开始入一个节点的时候,head和tail都指向第一个,之后,再来一个,尾指针后移。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                // 如果是1 就放在新的位置
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 让table的j位置存放链表的表头
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

对于resize的讲解我们算是结束了,大致就是三个小个分支,分别对应了三种情况

  • 原来的table已经用过了,意思就是已经有原来的容量了,此时扩容是2倍,并且阀值也是2倍增长,这种情况出现在什么时候?不断往hashMap表中加元素的时候,到达了阀值

  • 有阀值,但是没有表,这是一种初始化状态下,此时容量变成阀值的大小,新阀值为容量的0.75倍,出现在什么状态下呢?new(xxx)指明初始容量的时候 比如,我一开始指定了20容量,但是事实上它会经历几个转换过程

    • 先 计算出最接近20的2的指数倍,作为旧阀值,然后将旧阀值作为newCap新容量,之后 算出新阀值0.75*newCap,所以啊~我们指定的容量并不是真正的容量- -
  • 没有阀值,也没有表,也是一种初始化状态下,new()啥都没有,此时默认用16+12的组合

所以,可以看出来0.0 原来装填因子。。。用处不是那么大,初始化的时候有用也就是说,只干一次活,之后就直接成倍增长了。经过测试,不管是初始化16+12组合,扩容后就是32+24的组合,还是指定容量的扩容,都是两倍增长,一扫装填因子每次达到0.75再扩容的假象。至少我的实验结果是这个样子。

关于复制元素有几个示意图:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

get获取

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

明白了put,get就简单了,无非就是判断头节点,头节点判断完了之后遍历后面的节点,略过略过~不过需要记住,树的查找是O(logn),而链表的查找是O(n)还是红黑树哦~

个人觉得精髓部分就是put 和 resize还有hash这三个部分了,不得不说,实在是太牛比了!

总结

这里写图片描述

PS:都知道HashMap是无序的,这个无序的意思需要搞清楚,这里的无序指的是我插入的顺序不代表就是输出的顺序,而不是说,多次输出HashMap的元素位置就是随机变动的。举例子:

        HashMap<String, Integer> hashMap2 = new HashMap<>(20);
        hashMap2.put("3", 3);
        for (int i = 4; i < 27; i++) 
        {
            hashMap2.put(String.valueOf(i), i);
        }
        hashMap2.put("111", 1111);

输出:

{22=22, 23=23, 24=24, 25=25, 26=26, 111=1111, 10=10, 11=11, 12=12, 13=13, 14=14, 15=15, 16=16, 17=17, 18=18, 19=19, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 20=20, 21=21}

并不是我们输入的顺序,但是多次输出结果的顺序还是这个。看源码之后,散列提供了第一次位置的随机性,但是!位置第一次确定之后,就不能改了,不会因为你的输出而改变。JDK9是这样的不过。。python3好像是随机出现的,很奇怪

猜你喜欢

转载自blog.csdn.net/qq_41376740/article/details/80017585
今日推荐