30分钟解决面试HashMap的所有问题

 

       

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

 

面试官的灵魂拷问​ 

  1. 谈一下HashMap的特性?

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

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

  4. HashMap的扩容机制是什么?

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

  6. 为什么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<K,V>[] tab; Node<K,V> 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<K,V> 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<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) 
                            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<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;
            //这里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<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;
    }

扩容机制

扩容时高低链问题

先看代码是如何通过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. 本身首先是一棵二叉搜索树。

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

红黑树性质

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

  2. 根节点是黑色的;

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

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

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

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

其他常见问题

Q:什么是哈希碰撞?

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

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

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

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

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值