深入理解HashMap(一)

1,HashMap的概要

HashMap内部使用了哈希函数,是关联数组哈希表,是线程不安全的,它允许自己的key为null,也允许自己的value为空,遍历时无序.
其内部的哈希桶是数组,数组的话就会涉及到扩容操作,每个哈希桶都放的都是链表,链表的结点,就是hash表的元素.在JDK1.8中,当链表的结点个数达到8个时,就会将链表转化为红黑树,以提升它的查询和插入的效率
他实现的接口有Cloneable,Serializable, Map

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {

然后我们来谈谈扩容的问题,对应扩容我们涉及到一个值Threshold(阈值),当容量超过Threshold的时候,就会进行扩容操作,扩容的前后都是2的次方.因为都是2的次方,所以我们在通过key寻找value时,我们就可以通过位运算来代替普通的取模运算.
对应key对应的hash值,不仅仅是key.getHashCode()这个方法,还要经过扰动函数,使得Hash更加的均衡.HashCode的范围是40多亿,而且是int类型,是很发生碰撞的.
但是我们要考虑到,hashMap的桶远远要比Hash的取值范围要小很多,所以我们会根据桶的长度进行取余,忽略高位,只是用hash的地位,这样的话,碰撞的几率就会大了很多.
此时我们就需要引入扰动函数,它综合了高位和低位的特征,并全部都放在了低位,这样相当于高低位都进行了运算,通过这个来减少hash碰撞的几率,扩容操作时,会new一个新的Node作为hash桶,这是我们将原来所有的值,全部put到new的node中,重新做了一个put操作,性能消耗非常大,所以当Hash的内容越大时,性能消耗就越明显.
当发生过hash碰撞,且节点数小于8个(即在一同链表中),这是我们将结点放入新的hash中,有可能保持不动(low位),也有可能原位置+原哈希桶的容量(high位)

图片.png

在HashMap的源码中,有许多位运算代替常规运算的地方,以此来提升效率
Hash&(arr.length-1)代替了Hash%arr.length
通过if((e.hash&oldCap)==0)判断扩容之后e是在低区还是高区

了解完了HashMap的基础知识,我们开始来研究HashMap的源码吧

2,链表的结点类型

static class Node implements Map.Entry {
        final int hash;//此结点的hash值
        final K key;//此结点的key
        V value;//对应的value
        Node next;//因为是链表,所以要指向当前结点的下一个结点

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

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        //key的哈希值异或上value的哈希值
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //设置结点的value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry e = (Map.Entry)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

了解了Node的属性和方法,十分的清晰明了,我们只要知道其中的属性即可,因为是链表的数据结构,所以我们使用next

3,HashMap的基本属性

//最大的容量是2^30方
static final int MAXIMUM_CAPACITY = 1 << 30;
//初始化容量为2^4方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hash桶
transient Node[] table;
//加载因子,上面默认的加载因子是0.75f
 final float loadFactor;
//域值=加载因子*当前哈希桶的大小
int threshold;

4,HashMap的构造函数

//无参构造函数,只是将构造因子设置成默认的构造因子
  public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
//此构造函数,指定了HashMap的容量
 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);
         //如果超过了2^30次方,那就设置成最大值
        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这个方法,我们在进入到这个方法中

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;
        //上面的五行,经过运算使得每一位都是1,这是在此基础上再加上1,就一定是2的次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

然后我们来看最后一个hashMap的构造函数

 public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

第一句话设置了默认的构造因子,我们来详细的研究一下最后一行的方法,我们进入到其中

 final void putMapEntries(Map m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                //由加载因子计算出当前的阈值
                float ft = ((float)s / loadFactor) + 1.0F;
               //处理边界条件 
               int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果新的阈值大于原来的,那么返回一个满足2的次方的阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
           //如果传入的m的值大于当前的阈值,就会进行扩容操作
            else if (s > threshold)
                resize();
            //处理完其他的工作,就是将m中的key value依次的移动向当前的table
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

5,扩容操作

5.1 扩容操作的第一部分

final Node[] resize() {
        Node[] oldTab = table;//获取到当前的hash桶
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取到当前的长度
        int oldThr = threshold;//获取到当前的阈值
        int newCap, newThr = 0;
        //首先我们都newCap和newThr的进行分析
        if (oldCap > 0) {//如果原hash桶的容量不为空
            //边界分析,原容量已经超过了最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;//阈值变成了2^32  -1
                return oldTab;//直接返回,不用在创建了
            }//如果在最大值之内,那么新的容量就是旧的容量的2倍
             //如果旧容量大于初始容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //那么阈值就变成原来的二倍
                newThr = oldThr << 1; // double threshold
        }//当当前表示空的时候,新的表的容量直接就等于了就得阈值
        else if (oldThr > 0) 
            newCap = oldThr;
        else { //如果旧的表容量为空,阈值也是空的,那么新表就相当于是初始化一张Map            
            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;

总结一下,对于扩容的newCap,和newThr的修改如下:
(1),如果原表不是空的
(1),容量已经到了最大值,直接返回
(2),如果没有到达最大值,新容量为原容量的二倍,新阈值也是原阈值的二倍
(2),如果表是空的,但是有阈值
新的容量就是旧的阈值
(3),如果表示空的,也没有阈值
新的容量就是默认16,新的阈值就是默认12
(4),如果新的阈值是空的,那就根据新的容量计算出阈值出来

5.2 扩容的第二部分

我们这个时候已经获得了newCap和newThr的值,我们开始移动元素

      Node[] newTab = (Node[])new Node[newCap];//首先初始化新的Table
        table = newTab;//将当前类的对象的属性指向这个newTab,因为这是我们将来的HashMap中的桶
        if (oldTab != null) {//如果旧的桶不是空的
            for (int j = 0; j < oldCap; ++j) {//我们就开始遍历
                Node e;
                if ((e = oldTab[j]) != null) {//这时候e就是链表的头结点
                    oldTab[j] = null;//将原来的设置为空,这样jvm就会根据GC将其回收
                    if (e.next == null)//如果只有一个结点,此时不会发生哈希碰撞
                        //当前的hash值对newCap取模,这里我们通过位运算来加快运算
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//如果有8个以上的结点
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { //如果有节点当只有小于8个,这时候我们就是对链表进行操作,老的每一个桶的位置,都有可能进入新的低位和新的高位
                        Node loHead = null, loTail = null;//低位的Head
                        Node hiHead = null, hiTail = null;//高位的head
                        Node next;
                        do {
                            next = e.next;//用next保存链表
                            if ((e.hash & oldCap) == 0) {//如果当前预算结果得到的是0,那么
//就放在低位,如果当前运算结果是1,就放在高位,两种插入方式都是尾插法
                                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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6,putVal

我们回到上面的构造函数,其中有一段代码是将老结点放入新结点的

  for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }

我们深入理解一下putVal这个函数吧

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //当前的hash桶,p表示一个临时结点
        Node[] tab; Node p; int n, i;
       //如果当前的hash表是空的 
       if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//直接将扩容后的哈希桶的长度赋值给n
        if ((p = tab[i = (n - 1) & hash]) == null)//如果当前i的位置是空的,表示没有发生
//hash碰撞,直接构建一个节点,然后将它挂在index的位置就可以了
            tab[i] = newNode(hash, key, value, null);
        else {//如果发生了hash冲突
            Node e; K k;//如果是key相同,那么直接覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)//如果是红黑树的结构
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {//如果这时候是一个链表,我们就要遍历它
                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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//修改modCount
        if (++size > threshold)//更新size,并判断是否需要扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

以上就是hashMap的初始化过程,我们会在(二)中详细的介绍HashMap的增删改查的过程

7,小结

  • 运算通过位运算代替,更高效
  • 在进行扩容的过程中,记得将老结点的引用置为null,以便垃圾回收
  • 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
  • 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
  • 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
  • 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点
  • 如果追加节点后,链表数量》=8,则转化为红黑树
  • 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。
    参考自
    https://blog.csdn.net/zxt0601/article/details/77413921

你可能感兴趣的:(深入理解HashMap(一))