重新认识一下HashMap

1,数据结构

在jdk1.8之前HashMap采用数组+链表的结构,而在1.8的时候采用数组+链表+红黑树的结构,为什么要采用数组+链表+红黑树呢

  1. 数组查询效率快
  2. 链表是为了解决数组同一位置的冲突,还有一个就是删除,添加快
  3. 红黑树是在链表长度太大时,将链表改成黑红数可以大大提高效率

数据结构大概就是这个样子

4.

2,构造器

//第一个构造器:
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0,否则报错
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                            initialCapacity);
    // 初始容量不能大于最大值,否则为最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 填充因子不能小于或等于0,不能为非数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                            loadFactor);
    // 初始化填充因子                                        
    this.loadFactor = loadFactor;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);    
}
//第二个构造器
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//第三个构造器
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

第一个构造器有两个参数,一个是初始容量,一个是负载因子
第二个构造器只有一个初始容量的参数
第三个不需要参数,全部使用默认的

通过上面三个构造器,可以看出有这么几个参数是比较重要的:


    /**
     *数组的初始容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     *把链表转换为红黑树的阀值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 把红黑树还原成链表的阀值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     *最小树形化的容量阀值
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

看完构造器再来看一下常用的添加,查询,删除操作

3,添加(put):

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

第一步调用putVal(),里面hash(key)比较重要

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

这里主要用了一个异或运算,将key的hashCode值的高16位和低16位进行异或,目的就是为了解决Hash冲突,因为数组的默认容量为16,转换为二进制是10000,完整的hash是32位,这里只有五位,使用高16位和低16位异或一下就能充分把32位利用起来了,而且这个异或运算在效率上比起原来的取模运算效率更快

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            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 {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

初看一下这代码还挺吓人的,不过仔细一看,不就是一堆if,else吗,容我慢慢分析

1:五个变量

 int hash //哈希值,用于计算下标
 K key //键值
 V value //需要存储的Value值
 boolean onlyIfAbsent//是否替换原来的值,false为替换,true为不替换
 boolean evict //是否为创建模式

2:初始化桶

 Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

这里通过判断tab来决定是否进行初始化,而tab又是table赋值而来,通过前面的构造器我们并没有看见有对table初始化,所以这里会进入resize()方法,下面看一下他是怎么初始化的,在初始化的时候又做了些什么事情?

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    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;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
        @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) {
                    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
                        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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

本以为只是一个初始化,可看这代码不像啊,这也太复杂了吧,看来不仅仅是初始化这么简单啊,是的,其实这个方法的作用是扩容,初始化也当作扩容的一部分,别看他这么复杂,其实我们这次进来执行的代码就几句

   newCap = DEFAULT_INITIAL_CAPACITY;//桶的初始容量
   newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩容阀值,达到这个值就需要扩大桶的容量了,默认为0.75 * 16 = 12

下面才正式开始添加的步骤,之前都是准备
通过上面的数据结构我们可以看出有三种结构,分别是数组,链表,红黑树,在添加时我们把这三种结构分为了三种情况
第一种:数组

 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

第一种最简单,通过hash值计算出下标,然后判断桶中对应下标是否有值,当为null时则添加进去
第二种,红黑树

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

首先用桶下标对应值的key和现在需要添加的新key用equals进行比较,如果相同,则覆盖之前值,
判断桶下标存的值是否为红黑树,如果是则进行红黑树的节点添加

第三种:链表

	else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
                }
            }

循环链表的元素,通过next取得下一个元素,如果下一个元素为null,则把当前要添加的元素添加进去,添加过后判断链表元素是否达到8个,如果达到则把链表转换为红黑树,这里还有一个判断是,如果链表里面的key和新添加的key相同则直接退出,这里也是用equals做比较

最后一步,采用尾插法插入节点保存数据,并返回老数据,最后判断添加过后的容量大小是否达到扩容的阀值,如果达到则进行扩容

      if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);

再来看一下扩容操作

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;
            }
           //如果扩容后小于最大值,并且大于默认值16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 扩容为原来的两倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
        //创建一个新hash桶,将原来的hash桶传进去
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //用循环把原来hash桶的数据,插入到新hash桶里面去
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //原来hash桶数据只有一个元素,不是链表时
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                     //原来hash桶数据是一个红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //原来hash桶数据一个链表    
                    else { // preserve order
                       //把链表的值重新hash分布一遍
                        Node<K,V> loHead = null, loTail = null;//重新分布的索引为原索引
                        Node<K,V> hiHead = null, hiTail = null;//重新分布的索引为原索引+原hash桶容量大小值
                        Node<K,V> next;
                        //这里又使用一个循环来
                        do {
                            next = e.next;
                            //如果节点的hash于原来的桶容量大小等于0
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                             //如果e的hash值与老表的容量进行与运算为1,
                             //则扩容后的索引位置为:老表的索引位置+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),
                        //则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),
                        //则将最后 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

特别需要注意,在扩容的时候又一个判断来确定扩容以后元素的存放位置 if ((e.hash & oldCap) == 0) . 为什么要与原来的数组长度来做与运算呢 ?这其实也是为了让元素更加均匀的分布,就是用元素的 hash 值 与老的数组长度的与运算,当不等于0的时候就进行将元素迁移到 +oldCap 的位置上。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。

4获取(get)

在看完添加过程之后再来看获取,这时就很舒服了,上代码

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

和添加时一样,先把key使用hash算法hash一下,存的时候都是根据这个值存的,取的时候那肯定也需要这个值
下面看一下获取的主要过程

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //table不为null并且已经初始化了
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //根据hash算出下标,并且桶下标所对应的值不为null
            (first = tab[(n - 1) & hash]) != null) {
            //如果第一个节点的hash和要取出的hash值相同,则返回这个值
            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);
                    //不为红黑树时,就为链表,使用循环判断链表元素的key和要取出的key是否相同,使用equals来进行比较
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

判断表或key是否是null,如果是直接返回null
判断索引处第一个key与传入key是否相等,如果相等直接返回
如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
如果不是树,就遍历链表查找

5删除(remove)

  public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

老规矩,还是先计算一下hash值,然后交给另外一个方法

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;
        //先看一下桶是否有初始化
        if ((tab = table) != null && (n = tab.length) > 0 &&
           //再看一下桶里面有木有这个经过hash算出来的下标所对应的值
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //如果第一个节点的hash和要取出的hash值相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
                //为链表状态
            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)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //节点不为null
            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);
                //节点为为普通元素    
                else if (node == p)
                    tab[index] = node.next;
                //节点为链表    
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则判断节点的状态,如果为红黑树,则使用红黑树的方法进行删除,如果为普通元素则把下一个元素赋值给当前元素,如果为链表则把他的赋值给下一个,使要删除的这个元素失去引用

6总结:

  1. hash函数使用了hashCode的高16位异或低16,降低了hash冲突
  2. 使用 (n - 1) & hash 计算桶索引,提高了效率
  3. 使用链表来解决hash冲突,当链表长度有八个时,把链表转换为红黑树,删除后如果红黑树的元素为6还原成链表
  4. 插入时采用尾插法,解决了死循环的问题
  5. 每次扩容为原来的2倍,扩容后会将原来的元素进行重新hash分布,分布的位置为原来的索引或原来的索引加上原来桶的容量大小(table的长度始终为 2 的 n 次方;索引位置的计算方法为 “(table.length - 1) & hash”。)

参考文章:

  1. https://www.cnblogs.com/xiaoxi/p/7233201.html
  2. https://www.cnblogs.com/wuzhenzhao/p/13199350.html
  3. https://blog.csdn.net/qq_41345773/article/details/92066554

版权声明:本文为qq_41595149原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。