深入理解HashMap

​ 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK1.8的HashMap源码进行分析。

面试官的灵魂拷问​ 

谈一下HashMap的特性?

HashMap中hash函数是怎么实现的?

为什么HashMap数组的长度必须是2的N次幂?

HashMap的扩容机制是什么?

为什么红黑树和链表的转换阈值是6和8不是7呢?

为什么jdk1.8中新增的是红黑树而不是AVL?

什么是HashMap?

   HashMap是基于哈希表的Map接口的非同步实现,是一种用于key-value的映射关系的集合,并且key和value都允许为null,且不保证顺序性。在1.7的时候底层采用数组+链表实现,1.8底层采用数组+链表+红黑树实现(以下基于1.8)。

数据结构

基本常量

//默认初始化大小 1<<4=16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

//加载因子0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当链表长度大于等于该值时转换为红黑树

static final int TREEIFY_THRESHOLD = 8;

//当红黑树的大小小于等于该值时转换回链表

static final int UNTREEIFY_THRESHOLD = 6;

//转换为红黑树时当前整个数组的大小必须大于等于这个值

//才进行转换,可以去看看转换代码treeifyBin

static final int MIN_TREEIFY_CAPACITY = 64;

构造函数

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

    }

   //可传入初始容量

    public HashMap(int initialCapacity) {

        this(initialCapacity, DEFAULT_LOAD_FACTOR);

    }

    public HashMap() {

     // all other fields defaulted 0.75f

        this.loadFactor = DEFAULT_LOAD_FACTOR; 

    }

上面三个构造函数中第二个函数调用调用的是第一个构造函数,第三个是默认的无参数构造。下面主要讲第一个。

在构造函数HashMap(int initialCapacity, float loadFactor)中,前面对两个参数进行了范围验证,然后设置加载因子,并且结算下一次扩容的阈值,这里看一下源码

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;

    }

以上tableSizeFor()这个函数的意思就是返回一个>=给定数的2^N。

>>>表示不带符号向右移动二进制数。

|=表示按二进制位或操作,并赋值等价于a=a|b。

以下

/**

 *

 * 以正数8为例

 * 源码:

 * 00000000 00000000 00000000 00001000

 * >>>1

 * 00000000 00000000 00000000 00000100

 * |=1

 * 00000000 00000000 00000000 00001100

 * >>>2

 * 00000000 00000000 00000000 00000011

 * |=2

 * 00000000 00000000 00000000 00001111

 * >>>4

 * 00000000 00000000 00000000 00000000   到这里可以看出来,后面的计算是无效的

 * |=4

 * 00000000 00000000 00000000 00001111

 * -------------------------------------------------

 * 以正数7为例

 * 源码:

 * 00000000 00000000 00000000 00000111

 * >>>1

 * 00000000 00000000 00000000 00000011

 * |=1

 * 00000000 00000000 00000000 00000111

 * >>>2

 * 00000000 00000000 00000000 00000001

 * |=2

 * 00000000 00000000 00000000 00000111

 */

上面整个计算过程的意思就是从高位到低位第一个1后面的数字全部置1。

为什么要把第一个1后面的数字全部置1?因为2在计算机中具有特殊的地位。如果一个数等于2^N,这个数的二进制表示肯定只有一个1(不包括符号位)。

代码一开始为什么要减1?可以看到上面正数8进行5次移位或等的结果是15,与预期初始化长度为8大了一个级别。这里减掉一个1,在进行移位运算的时候将自己本身也包含进去。

前面移位运算的结果总是2^N-1,所以在函数返回的时候+1,刚好等于2^N

hash函数

  static final int hash(Object key) {

        int h;

        return (key == null) ? 0 :

        (h = key.hashCode()) ^ (h >>> 16);

    }

首先进行了>>>16位,也就是将高16位移动到低16位, 然后^按位异或(同为0,不同为1),其运算结果高地位都参与进去了,增加了随机因子。

插入原理

因为put函数调用的直接就是putVal()方法,所以这里直接看putVal(),代码挺长;

putVal()源码注释

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                   boolean evict) {

        Node[] tab; Node p; int n, i;

        //判断数组是否存在和数组长度

        if ((tab = table) == null || (n = tab.length) == 0)

            //创建新的数组

            n = (tab = resize()).length;

         //判断该地址是否存在元素节点,并将p指向地址

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);//创建新的节点

        else {

            //该下标已存在节点

            Node e; K k;//声明临时节点

            if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))

                //如果元素的hash相同且equals相等,那么判定为同一个元素

                e = p;

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

                        // 判断是否满足转换为红黑树

                        if (binCount >= TREEIFY_THRESHOLD - 1) 

                            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;

    }

执行流程图

扩容机制

resize()源码

  final Node[] resize() {

        Node[] 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;

            //这里0.75*数组长度

            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[] newTab = (Node[])new Node[newCap];

        table = newTab;

        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {

                Node 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)e).split(this, newTab, j, oldCap);

                    else { // preserve order

                        //高低链,高链需要迁移,低链不用

                        Node loHead = null, loTail = null;

                        Node hiHead = null, hiTail = null;

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

    }

扩容机制

扩容时高低链问题

先看代码是如何通过hash定位数组下标的·

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

首先知道数组的长度永远是2^N次方,转换为2进制,只有1个1,(2^N)-1,二进制从2^N这一位的后一位开始为后面全是1,采用这种方式分布也是可以让不同的hash值定位到相同的下标上面。(注:同一个链表里面的所有节点hash不一定相同,相同hash的肯定定位到同一个链表)

再看看如何区分高链和低链的

   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;

    }

上面代码就是hash & oldCap时=0时就是低链,!=就是高链,将高链偏移一个原始数组的位置即可。

(注:这里(2^N)-1和(2^N-1)在二进制的时候除了第一位一样,其余位全部不一样,所以偏移一个原始数组位置)

为什么转换阈值不是7?

直接看什么时候转换的

   //插入时

   if (binCount >= TREEIFY_THRESHOLD - 1)

        treeifyBin(tab, hash);

//转换或者移除时,很多地方大差不差

 if (lc <= UNTREEIFY_THRESHOLD)

      tab[index] = loHead.untreeify(map);

转换红黑树条件>=7,转换回来条件<=6,插入的时候是已经存在了7个,当前插入的算一个,相当于间隔了一个的。如果是7的话,那么链表和红黑树之间的切换范围值就太小了。如果我的链表长度不停地在7和8之间切换,那岂不是得来回变换形态?所以选择6是一种折中的考虑。

为什么jdk1.8选用红黑树?

AVL树性质

本身首先是一棵二叉搜索树。

带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)

红黑树性质

每个节点,不是红色就是黑色的;

根节点是黑色的;

如果一个节点是红色的,则它的两个子节点是黑色的(没有连续的红色结点,可以有连续的黑色节点,这里意味着最长路径最差情况是最短路径的1倍)

对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。(每条路径的黑色结点个数相等)

每个叶子节点都是黑色的(这里的叶子节点是指的NIL节点(空节点)) 优缺点:因为不要求严格平衡,在查询效率上比AVL少许,但是在插入效率上比AVL效率高,这里增删改差的效率比较平衡;

整体来说:AVL树侧重于搜索,红黑树因为不要求严格彭亨,在插入和搜索两个功能上性能比较均匀,所以选用红黑树。

其他常见问题

Q:什么是哈希碰撞?

A:HashMap中2个对象产生相同的hashcode时就是哈希碰撞

Q:HashMap是如何解决哈希碰撞?

A:产生哈希碰撞的对象,以链表的形式存储在数组的同一个位置上

Q:HashMap的数组中同一链表上对象的hashcode都一样的吗?

A:不是,而且大多数情况下都不一样

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