HashMap比较重要的源码分析

数据结构

在这里插入图片描述

上图有一个错误——链表转为红黑树:链表长度大于等于8时,并且数组长度大于64时,链表转为红黑树。

属性


int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始化默认容量==16
int MAXIMUM_CAPACITY = 1 << 30;//最大容量
float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
int TREEIFY_THRESHOLD = 8;//转为红黑树阈值
int UNTREEIFY_THRESHOLD = 6;//转为链表阈值
int MIN_TREEIFY_CAPACITY = 64;//扩容——数组——最小阈值
transient Node<K,V>[] table;//数组存放的是节点或者链表或者红黑树
transient Set<Map.Entry<K,V>> entrySet;
transient int size;//元素数量
transient int modCount;//修改次数
int threshold;//扩容门槛
final float loadFactor;//加载因子

构造方法

分别为:无参、 带容量大小和加载因子的构造方法、仅带容量大小的构造方法、 带一个Map的构造方法

  //其实构造方法没意思,就是设置一个加载因子和容量
  //无参构造
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //设置加载因子 = 0.75
    }
    //仅带容量大小的构造方法
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //带容量大小和加载因子的构造方法、
    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);
    }
    // 带一个Map的构造方法
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

添加K-V

主要分两部分:
1、根据key得到hash值,再根据hash值结合数组长度进行模运算,得到一个 int 值(A),这个 A值就是 Entry 要储存在数组的位置(下标)。
2、数组下标A没有Entry,没有值,使用 resize 方法初始化,直接生成新的节点在当前索引位置上
3、数组下标A有Entry,接下来就是我们去解决hash冲突。
4、记录 HashMap 的数据结构发生了变化
思路:
科普:其实 K-V 存储到HashMap数组中后会变成 Entry对象

    HashMap<String, Integer> hashMap = new HashMap<String, Integer>();
    hashMap.put("1",1);
        
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //计算key的hash
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    //参数:hash:通过 hash 算法计算出来的值。onlyIfAbsent:默认 false,在默认情况下,添加数据,如果出现相同key,后添加的数据会覆盖之前的数据。evict:如果为false,则表处于创建模式。
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        //n 为数组长度、i为数组索引下标、p为i下标位置的 Node 值、tab==table
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //1、如果数组为空,使用resize()初始化数组(2的幂展开数组容量)
        if ((tab = table) == null || (n = tab.length) == 0)//table存储 Entry 的数组
            n = (tab = resize()).length;
        //2、如果当前索引位置是空的,直接生成新的节点在当前索引位置上
        if ((p = tab[i = (n - 1) & hash]) == null)//当前索引:key计算出来的数组坐标===(hash&(n-1))。注意:随着数组容量 n改变,key可以取的范围也在变大,但是不会超出 n的取值访问。
            tab[i] = newNode(hash, key, value, null);//hash.key、value、next
         //3、当前索引位置不为空,接下来就是我们去解决hash冲突
        else {
            /*
            *解决hash冲突
            *  如果key的hash和值都相等、直接把当前下标位置的Node值赋给临时遍历e。
            *  如果p是红黑树:红黑树添加节点
            *  如果p是链表
            *  找的新节点位置后的操作:
            */
            Node<K,V> e; K k;//e 当前节点(不是数组下标的Entry)的临时变量
            //数组下标为i下标位置的 Node 值等于新节点。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)//p是红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//p是链表,添加节点几种情况(无非就是修改或添加节点):frist.next = null、链表中有和新节点相同的节点、整个链表没有相同新节点
                for (int binCount = 0; ; ++binCount) {//binCount 一个计数器
                    if ((e = p.next) == null) {//链表的第二给节点为空,说明链表中没有找到相同的Node,所以尾部添加新节点即可
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) //当链表长度大于等于8时,链表转为红黑树。 TREEIFY_THRESHOLD = 8 
                            treeifyBin(tab, hash);
                        break;
                    }
                    //链表某给节点位置与新建节点相同,跳出遍历。修改e.value=value即可
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //新来节点与之前节点相同,后来节点值覆盖原来节点的值,即e.value=value
            //头节点或链表中的节点有可能会发生Node相同的情况。
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//因为有相同节点,后来节点值覆盖原来节点的值
                afterNodeAccess(e);//用来回调移除最早放入Map的对象
                return oldValue;
            }
        }
        //4、记录 HashMap 的数据结构发生了变化
        ++modCount;
        if (++size > threshold)//判断是否需要扩容——扩容条件:元素数量到达容量的四分之三,数组开始扩容,容量为2的幂次方
            resize();
        afterNodeInsertion(evict);//HashMap中没有对这个方法做操作。
        return null;
    }




//内部红黑树类,不止putTreeVal一个方法,还有许多,有兴趣可以查看Hashmap源码内部类TreeNode·
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>{
    TreeNode<K,V> parent; 
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;   
    boolean red;
    //寻找根节点
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
             if ((p = r.parent) == null)
                return r;
                r = p;
        }
    }

    final TreeNode<K,V> putTreeVal(
                         HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
            /*
            * map:当前Hashmap对象、tab:Hashmap对象中的table数组
            * h:hash值
            */
            Class<?> kc = null;
            boolean searched = false;//标记是否被检索过
            TreeNode<K,V> root = (parent != null) ? root() : this;//根节点
            for (TreeNode<K,V> p = root;;) {//从根节点除开始遍历
                int dir, ph; K pk;//dir为-1:放在左边、die为+1:放在右边
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))//要放进去key在当前树中已经存在了(equals来判断)。 直接返回该节点的引用 外边的方法会对其value进行设置
                    return p;
                //说明进入下面这个判断的条件是 hash相同 但是equal不同。接下来就是解决hash冲突。
                //自己实现的Comparable的话,不能用hashcode比较了,需要用compareTo
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    //在左右子树递归的寻找 是否有key的hash相同  并且equals相同的节点
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;//找到了key在当前树中已存在,返回已存在的节点的引用 外边的方法会对其value进行设置
                    }
                    dir = tieBreakOrder(k, pk);//说明红黑树中没有与之equals相等的  那就必须进行插入操作。
                 }

                /**
               新节点安放好需要做一下一个操作:
               1、对于红黑树要做的操作
                   1.1、父节点.left=新节点、或者父节点.right=新节点
                   1.2、新节点.parent=父节点
               2、对于节点要做的操作
                   2.1、父节点.next=新节点
                   2.2、新节点.prev=父节点
                **/
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {//获取父节点的子节点,并让子节点等于父节点
                    Node<K,V> xpn = xp.next;//获取三级节点
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//newTreeNode(hash、key、value、prev)。创建一个新节点。然后根据hash值将新节点放于对应位置。
                    if (dir <= 0)
                        xp.left = x;//新节点位于二级节点的左边
                    else
                        xp.right = x;//新节点位于二级节点的右边
                    xp.next = x;//TreeNode.next=新节点
                    x.parent = x.prev = xp;//新节点.prev=父节点、新节点.parent=父节点
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    //moveRootToFront 方法是把算出来的root放到根节点上。
                    //balanceInsertion 对红黑树进行着色或旋转,以达到更多的查找效率
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
    }

    static int tieBreakOrder(Object a, Object b) {
        int d;
        if (a == null || b == null ||(d = a.getClass().getName().d  = 
              (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
            return d;
     }
}

查询

一、haha不冲突情况下:
1、获取数组中节点A时(发现hahs值相等,并且equal也相等),返回该节点A
二、hash冲突情况下(链表、红黑树):
1、获取到某一个节点A(发生Hash相等,但是equals不等)
2、链表:直接死循环,找到相等节点并返回该节点,或者没有找到该节点,返回null
3、红黑树:使用红黑树查询方法(不清楚,下回研究)

    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;
        //数组部位null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //第一张情况,不发生hash冲突
            if (first.hash == hash &&
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //第二种情况,发生hash冲突
            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;
    }

面试题

HashMap数据结构和底层原理
HashMap的扩容方式?负载因子是多少?为什是这么多?
答:先创建一个新的Entry空数组,长度是原来的两倍,重新遍历员Entry数组,把所有的entry重新Hash到新数组中。负载因子也称扩容因子等于 0.75f ,因子太小,会导致频繁扩容,浪费资源,太大会增加Hash冲突。
HashMap的主要参数都有哪些?
HashMap是怎么处理hash碰撞的?
答:使用链表和红黑树来解决hash碰撞。
hash的计算规则?
答: ( h = key.hashcode())^(h >>> 16)
你提到了还有链表,为啥需要链表,链表又是怎么样子的呢?
为啥改为尾部插入呢?
答:因为之前的使用hsahmap在并发+扩容情况下,会造成扩容死循环。
什么时候resize呢?
答:第一次初始化+以后每一成扩容
扩容?它是怎么扩容的呢?
为什么要重新Hash呢,直接复制过去不香么?
答:因为数组长度改变,导致hash取下表时规则也改变,所有要重写Hash,而不是直接复制过去
说完扩容机制我们言归正传,为啥之前用头插法,java8之后改成尾插了呢?
但是你都都说了头插是JDK1.7的那1.8的尾插是怎么样的呢?
那我问你HashMap的默认初始化长度是多少?
答:16
你那知道为啥是16么?
答:1<<4
那为啥用16不用别的呢?
答:2的幂,这样是为了位运算的方便,位运算比算术计算的效率高很多,之所以选择16,是为了服务将key映射到index的算法。
为啥我们重写equals方法的时候需要重写hashCode方法呢?你能用HashMap给我举个例子么?
我记得你上面说过他是线程不安全的,那你能跟我聊聊你们是怎么处理HashMap在线程安全的场景么?
对上面面试题有自己的理解,那差不多就掌握了面试过程中的HashMap。如有不懂可以提出,我会解答。


扩容

用到这个方法有两处:初始化、添加元素后数组扩容。

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 超过最大值就不再扩充了,就只好随你碰撞去吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 没超过最大值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        }
        else if (oldThr > 0) 
            newCap = oldThr;
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 计算新的resize上限
        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) {
            // 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    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 { //链表优化重hash的代码块
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }


红黑树数据结构

红黑树添加节点===插入节点与二查树插入节点一致(左边插入的节点值<根节点值,有边插入的节点值>根节点值)。

//Treemap类
public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key);

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);//修复红黑树———>即:使不平衡的红黑树----》修改后满足红黑树五个性质。
        size++;
        modCount++;
        return null;
    }

插入节点后修复红黑树:染色解决方案、旋转解决方案、染色旋转解决方案。——————这就是数据结构的魅力————一个方法完成三件事。

红黑树节点——Entry属性:
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;

private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
        return (p == null ? null: p.parent);
}

private void fixAfterInsertion(Entry<K,V> x) {//x为要新增的节点
        x.color = RED;//红黑树新节点变为红色

        while (x != null && x != root && x.parent.color == RED) {//节点不为空、不是根节点、x的父节点为红色
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {//判断父节点是否在爷节点左边
                //父节点在爷节点左边
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));//获取叔节点
                if (colorOf(y) == RED) {//如果叔节点是红色—》那么将父节点变为黑色,将叔节点变为黑色,并且将爷节点变为红色、并且让爷节点变为X,然后把爷爷节点看作是新增节点,继续朝上进行染色 
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {//叔节点是黑色
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);//左旋转
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));//右旋转
                }
            } else {//父节点在右边
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));//获取叔节点
                if (colorOf(y) == RED) {叔节点是红色色
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {//叔节点是黑色
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);//右旋转
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));//左旋转
                }
            }
        }
        root.color = BLACK;
    }

红黑树的必须满足几个特性:节点必须是红色或者黑色、根节点必须是黑色、叶节点必须是黑色、红色节点必须有两个黑色儿子节点、任意节点到叶子节点,其中经过的黑色节点是相等的。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值