HashMap详解

前言

基于的版本为JDK1.8的,不过红黑树的方法详解,不会在这里去说,之后会有数据结构,来对此解释,HashMap的源码,涉及了大量的二进制的操作,不了解的,可以看下我发的二进制的解释。

构造器:

HashMap有四个构造器,这里介绍了三个常用的构造器。

public ExHashMap() {
        //加载因子,默认为0.75,用于确定临界值的
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

    public ExHashMap(int initialCapacity) {
        //加载因子,默认为0.75,用于确定临界值的
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public ExHashMap(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);
    }

tableSizeFor:
最后一个构造器里使用到了tableSizeFor方法,这个方法用于初始化threshold的变量,该变量的意义代表临界值的意思,当size的实际的数组大小大于threshold时,进行扩容。

//返回的一定是一个2次幂的数
    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的数,返回的就是一个2次幂的数,返回的数为cap的最小2次幂,即cap=15返回16,cap=16返回16,cap=17返回32。

int n=cap-1; 

作用:是为了防止当cap已经是2次幂的时候,返回的确是这个cap的2倍,减1则保证了返回的一定是最小的2次幂(可以看完这个方法的整个介绍后,在回来看)

之后进行移位操作,并且进行或的操作,这里给个实例看,方便大家理解。

cap=18 =====>0001 0010
n=17 ======>0001 0001

n>>>1=0000 1000
n |= n>>>1 ======>0001 0001 |= 0000 1000=0001 1001

n>>>2=0000 1100
n |= n >>> 2 =====>0001 1001 | 0000 1100=0001 1101

n >>> 4=0000 1110
n |= n >>> 4 ====>0001 1101 | 0000 1110=0001 1111

.....

可以看到到n |= n >>> 4时,n的二进制就已经是 xxxx 11111的形式了,之后的 n |= n >>> 8 、n |= n >>> 16已经不会对其有任何影响了,之后在返回的时候,还会有一个n+1的操作,经过这个操作时,n就会变成 xxx1 000的形式,此时返回的一定是一个2次幂的数。

其实这个方法的操作,将传入的值(不为2次幂时)从最大位数开始,就通过移位和或的操作,将之后的位数的值,全部变为1的操作,之后对移位后的最终值进行一个加1,则最高位数的上一位变为1,之后所有位数的值都为0.

而当传入的值是2次幂时,因为有减1的操作,即最高位n变为0,其余位数为1,之后的操作,不会对其有任何影响,因为其余位数已经为1了,之后在加1,这时最高位,又位1了其余位则为0,等于之前传入的值。

源码中移位操作,每次移的位都是前面的两倍的原因是,第一次移位后,进行或操作,就产生了,两个连续的1,第二次移位后,将上一步的两个连续的1,移动2位后,进行或操作,就可以产生4个1,即每次操作后都将产生上次2倍的移,在将这些1移动2倍的位数,就可以最快的完成目标。

那么为什么一定要返回2次幂的值呢,好处是:
1.提高内存运算速度
2.增加散列度,降低冲突(Length - 1的值的二进制所有的位均为1,这种情况下,Index的结果等于hashCode的最后几位。只要输入的hashCode本身符合均匀分布,Hash算法的结果就是均匀的。)
3.减少内存碎片(操作系统每次申请内存,都是2的倍数)

虽然这里是将返回的值,赋给threshold。但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

Put方法:

Put方法也是其中较难理解的一个方法,先上代码

 public V put(K key, V value) {
        //1.key的hash之后用于计算下标
        //2.键 3.值
        //4.当键相同时,是否会覆盖值,false则会覆盖当前值
        //5.HashMap中没有用到,不用思考
        return putVal(hash(key), key, value, false, true);
 }

hash方法:

 //该方法求出该key值加工后的hash值
    static final int hash(Object key) {
        int h;
        //得到hashCode值后,还要将高低16位进行一次异或,用来减少发生碰撞,
        //避免之后数组的长度过短,导致hash高位值的作用不大
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

先取到Key的hashCode,在和移位16位后的数组,进行一个异或操作,返回一个加工后的hash值。

这里之所以进行一个异或操作,并且是对移位16位后的数的原因是,避免导致hashCode的高位值,在取模计算时,没有任何的作用。

实例:

//不对其进行异或操作
h=10010100 00011010 10010010
//n表示数组长度减一后的值
n=00000000 00000000 00001111
h & n=00000000 00000000 00000010

h1=11000110 00101000 11010010
n=00000000 00000000 00001111
h1 & n=00000000 00000000 00000010
//h h1两次值不同,但计算出的下标相同,容易发生hash碰撞,分布的并不均匀

//对其进行异或操作
h=0001 1010 1001 0010
h=h ^ (h>>>16) ========> 10010100 00011010 10010010 ^ 0000000 00000000 10010100=10010100 00011010 00000110
n=00000000 00000000 00001111
h & n=10010100 00011010 00000110 & 00000000 00000000 00001111 =====>00000000 00000000 00000110

h1=11000110 00101000 11010010
h1=h1 ^ (h1>>>16) ========> 11000110 00101000 11010010 ^ 0000000 00000000 11000110=11000100 00101000 00010100
n=00000000 00000000 00001111
h1 & n=11000100 00101000 00010100 & 00000000 00000000 00001111======>00000000 00000000 00000100
//h h1两次值不同,但计算出的下标不同,主要针对数组长度不大,而hash的计算只和低位数有关的情况

putVal()方法:
该方法的大致思路是:
1.首先判断数组是否初始化,如果没有则对其扩容
2.之后对加工后的hash进行取模计算,得出对应的下标
3.如果下标处没有插入值,则直接插入
4.如果下标处已经有值了,说明发生了hash碰撞
5.判断头节点的Key值是否为当前需要插入的值,如果是记录当前头节点信息
6.判断数组中存储的是否为红黑数组结构,如果是得到该类型
7.循环遍历链表中的数据,找到了Key相等的节点,先将其记录下来,没有找到,说
明链表中还未插入,则直接插入,插入时需要判断是否达到链表的临界值,如果
达到需要将其转化为红黑树。
8.循环结束后,判断记录的值是否为空,不为空的话,根据onlyIfAbsent值,判断是
否直接覆盖当前节点的value值。
9.如果数量超过临界值,则扩容。

        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        //如果用于存储的数组还未初始化
        if ((tab = table) == null || (n = tab.length) == 0) {
            //则选择扩容
            n = (tab = resize()).length;
        }
        //计算下标的方法为:键的hash值按照数组的长度取余后,得到下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果该下标处,还未添加对应的链表,则直接赋值即可
            tab[i] = newNode(hash, key, value, null);
        else {
            //该下标位置处,已经有了同样的hash值的元素,需要构建链表或红黑树
            Node<K, V> e;
            K k;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                //如果链表的头节点的key和插入的一致,则先将其记录下来
                e = p;
            }
            else if (p instanceof TreeNode)
                //如果是红黑树的结构
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //需要遍历整个链表,找到与他Key相等的节点
                for (int binCount = 0; ; ++binCount) {
                    //如果下个节点为空,则说明该KEY还未插入链表中
                    if ((e = p.next) == null) {
                        //直接将其插入到尾节点中去
                        p.next = newNode(hash, key, value, null);
                        //如果链表的长度大于临界值的话,将其转化为红黑树结构
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果找到了KEY值相等的节点,直接跳出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
                //如果记录的值不为NULL,说明链表中有相等KEY的节点
                if (e != null) { // existing mapping for key
                    //得到旧值
                    V oldValue = e.value;
                    //根据onlyIfAbsent参数去判断,是否覆盖旧值
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    //HashMap中为空方法
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

resize():

//扩容,新数组的大小为旧数组大小的两倍,左移一位
    final Node<K, V>[] resize() {
        //得到旧的数组,也就是当前数组
        Node<K, V>[] oldTab = table;
        //旧的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧的临界值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果旧的容量大于0,已经初始化过的
        if (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; // double threshold
        } else if (oldThr > 0) {
            //临界值已经有了,新的容量等于当前临界值,一定为2的幂数
            newCap = oldThr;
        } else {
            //都为0
            newCap = DEFAULT_INITIAL_CAPACITY;
            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;
        //创建扩容后的数组
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;

        //接下来的操作是将旧数组里的数据,按照同样的计算下标的方法放入新的数组中,
        //避免扩容后,get不到对应的值了
        if (oldTab != null) {
            //循环得到旧数组中的数据
            for (int i = 0; i < oldCap; i++) {
                Node<K, V> e;
                if ((e = oldTab[i]) != null) {
                    //清除旧数组中的值
                    oldTab[i] = null;
                    if (e.next == null) {
                        //如果当前链表只有一个节点,直接添加
                        newTab[e.hash & (newCap - 1)] = e;
                    } else if (e instanceof TreeNode) {
                        // 如果该节点是树
                        // 调用红黑树 的split 方法,传入当前对象,新数组,当前下标,老数组的容量,目的是将树的数据重新散列到数组中
                        //((TreeNode)e).split(this, newTab, i, oldCap);
                    } else {
                        //说明该链表不止一个节点
                        //用于记录不用更换的下标的链表的头尾
                        Node<K, V> loHead = null, loTail = null;
                        //用于记录更换了的下标的链表的头尾
                        Node<K, V> hiHead = null, hiTail = null;
                        //记录下一个节点
                        Node<K, V> next;
                        do {
                            next = e.next;
                            //如果等于0,则说明不用更换下标
                            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) {
                            //将尾节点之后的节点置为null
                            loTail.next = null;
                            //下标和之前一致,直接等于头节点
                            newTab[i] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            //新的下标等于旧下标加旧的容量
                            newTab[i + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

该方法中,需要注意的几个点:
1.新扩容的大小为旧容量的两倍,如果没有初始化,则等于默认的长度16,此时临界值为12.
2.if ((e.hash & oldCap) == 0) ,关于这个判断的解释为:如果等于0,则说明不用更换下标,
详细解释为,oldCap一定为2次幂,如果其最高位为n,则n的值肯定为1,如果 e.hash & oldCap==0,则说明e.hash的n位数上的值,肯定也为0,扩容后为旧容量的两倍,即 n位为0,n+1位为1,取模时需要长度减一,n+1位为0,n位开始后的所有数都为1,此时和旧容量不同的是,新容量的n位为1,旧容量的n位为0,如果此时e.hash的n位的值为0时,则两次计算一致。
实例:

oldCap=0001 0000
n=0000 1111
h=0100 1001
h & n=0000 1001

newCap=0010 0000
n1=0001 1111
h=0100 1001
h & n1=0000 1001 

remove和get

接下来两个方法都较为简单,也比较相似,就不过多解释了

public V get(Object key){
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    //得到对应hash值中对应的key的值
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断数组是否为空,长度是否为0,并且根据KEY的hash值取到对应的下标值
        //得到数组中链表
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            //如果头节点的KEY和传入的KEY值,相等:则直接返回该节点
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果头节点的下个节点不为NULL,则继续寻找
            if ((e = first.next) != null) {
                //如果为红黑树结构
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //遍历整个链表寻找对应KEY值的节点
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }

    final Node<K,V> removeNode(int hash, Object key, Object value,
                                       boolean matchValue, boolean movable) {
        //定义一个节点数组,当前节点,数组长度,索引值
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果该数组不为NULL,长度大于0,取出对应的节点,赋给P
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //返回的节点,临时节点,键值
            Node<K,V> node = null, e; K k; V v;
            //如果当前KEY值匹配上了,则说明头节点是对应要删除的节点,先记录下来
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            //如果该头节点下还有节点,即发生了hash碰撞
            else if ((e = p.next) != null) {
                //判断是否为红黑树
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //循环整个链表,找到对应的节点
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            //如果存在该KEY值相等的节点,记录下来,跳出循环
                            node = e;
                            break;
                        }
                        //注意p记录的是下一个节点的父节点,即当找到了对应KEY值相等的节点时,p为node的父节点
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //根据matchValue是否为true,来判断是否需要根据value值是否相等来删除
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                //判断是否为红黑树
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //node等于p时,说明头节点就是我们需要删除的节点,直接将该节点从链表中剔除即可
                else if (node == p)
                    tab[index] = node.next;
                else
                    //p为node的父节点,此时将p的下一个节点,定义为node的下一个节点即可
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

总结:

1.关于HashMap的源码中的红黑树部分的方法,这里没有写,但之后一定会补上的,欢迎关注。
2.关于上面的注释、例子和说明,肯定会有一些不对的地方,欢迎朋友们指出,不是特别清楚的地方也欢迎讨论。
3.接下来还要LinkedHashMap和一些集合并发的源码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值