HashMap面试集锦

1、基本特性&原理

1.1:以键值对形式存在,键和值允许为空。其存储的数据结构为哈希表,元素的存取顺序不能保证一致性。在jdk1.7以前哈希表的底层是采用数组、链表结构实现,但在jdk1.8后是采用数组、链表(处理冲突)与红黑树实现。当链表的单个长度超过阈值8并且长度不能小于转红黑树tab最小长度,则将链表转换成红黑树(防止哈希碰撞攻击)。降低时间复杂度,从而实现查询效率提升:
1.1.1:通过hashCode()方法定位元素位置 ,避免遍历寻找元素位置从而降低时间复杂度
1.1.2:采用邻接链表+桶排序+红黑树

桶排序思想:若所有的桶(将一类数放一起)装满,扩大内存空间,将桶的数量扩大为原来的一倍,然后进行重新排序,从而降低时间复杂度

1.2:是非线程安全的,因为没有加锁。在1.7采用的是头插法,容易导致循环链表发生;1.8优化为尾插法,不会导致循环链表问题,但是可能同时插入会造成数据丢失

2、put值过程

2.1、jdk1.7 – put值主要源码分析

过程简述:

  1. 如果哈希表还未创建,则调用inflateTable()初始化
  2. 如果键为null,那么调用putForNullKey()插入键为null的值
  3. 如果键不为null,计算hash值并得到桶中的下标,然后遍历桶中链表,找到目标节点则替换旧值
  4. 如果没有找到目标节点,则调用addEntry()插入新节点
基本属性
	//默认初始容量16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//最大容量2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空Entry数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    //HashMap存储的键值对数
    transient int size;
    //阈值 = 容量*加载因子
    int threshold;
    //实际加载因子
    final float loadFactor;
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            //判断table是否为空,为空则进行初始化(entry数组分配空间)
            inflateTable(threshold);
        }
        //进入putForNullKey()可看出HashMap的key是允许为空的
        if (key == null)
            return putForNullKey(value);
        //调用hash()方法计算当前key的哈希值
        int hash = hash(key);
        //将该key的hash值与数组长度进行&运算,计算出当前key的数组存放下标
        int i = indexFor(hash, table.length);
        //遍历、判断新旧值的hash值与key是否相等,相等则覆盖旧值并返回旧值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //保证并发访问异常快速响应
        modCount++;
        //新增一个entry数组
        addEntry(hash, key, value, i);
        return null;
    }
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //当前map中键值对数大于等于阈值并且将要发生哈希冲突时,触发resize()扩容,否则直接调用createEntry()进行新增
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            //扩容后重新计算下标数组下标
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
void createEntry(int hash, K key, V value, int bucketIndex) {
        //获取待插入元素位置
        Entry<K,V> e = table[bucketIndex];
        //将待插入元素指向原有元素(即:头插法,保证每个新元素在第一位,同时将链表整体下移)
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //map中键值对数加1
        size++;
    }
void resize(int newCapacity) {
        //newCapacity = 扩容后的容量,获取原数组数据&容量值
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //判断原数组容量值是否等于最大容量值,是则修改阈值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //new一个扩容后的新数组
        Entry[] newTable = new Entry[newCapacity];
        //将旧数组的数据与扩容后的新数组进行转换
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历旧数组中的所有entry数组
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                //如果需要重新计算hash值(initHashSeedAsNeeded()决定)
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //获取新的下标
                int i = indexFor(e.hash, newCapacity);
                //头插法
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
2.2、jdk1.8 – put值主要源码分析

过程简述:
1、如果table数组为空则调用resize()
2、如果不为空通过计算出来的索引位置判断是否为空,是则调用newNode()新增节点
3、否则不为空,即:将发生hash冲突
3.1、如果p节点与传入的hash值和key相等,则为目标节点,开始覆盖
3.2、如果不相等并且p节点是树节点,则调用putTreeVal()查找目标节点,开始覆盖
3.3、否则为普通链表,通过尾插法追加节点元素,当链表节点数大于阈值8则调用treeifyBin()转红黑树


基本属性
	//默认初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量2^30
    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;
    //转红黑树tab最小长度
    static final int MIN_TREEIFY_CAPACITY = 64;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> p; int n, i;
        //若table为空或table的长度为0,则调用resize()进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //通过hash值计算下标位置,并将下标位置的头结点赋值给p,若p为空,则在该索引位置新增节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //存在哈希冲突
            java.util.HashMap.Node<K,V> e; K k;
            //若p节点的key和hash值与传入的相等,则p节点为目标节点,将p赋值给e
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //若p节点是树节点,则调用putTreeVal()方法查找目标节点
            else if (p instanceof java.util.HashMap.TreeNode)
                e = ((java.util.HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //否则为普通链表节点,遍历链表,binCount统计链表节点数
            else {
                for (int binCount = 0; ; ++binCount) {
                    //当p的next节点为空时,则新增节点(尾插法)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //当链表节点数大于等于链表转红黑树阈值-1时,调用treeifyBin()将链表结构进行树化或扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //当节点e的key和hash值与传入的都相等,e即为目标节点,跳出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //map中已经存在该key,覆盖原值并返回原值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //当插入的节点数大与阈值,则调用resize()进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3、扩容机制

jdk1.7:
1、new一个扩容后的数组
2、调用transfer()方法,遍历原来数组的链表上的所有元素
3、获取每个元素的key,并重新计算在新数组上对应的下标值
4、将该元素放到新数组对应的下标上
5、遍历完后,将新数组赋值给table对象

jdk1.8:
1、扩容则new一个新数组
2、遍历原来数组的链表或红黑树上的所有元素
2.1:当为红黑树时:
遍历红黑树上所有的节点,计算每个元素在新数组的下标位置并统计个数;
当元素个数大于阈值8,则生成新的红黑树;否则生成链表
2.2:当为链表时:
重新计算所有元素在新数组中的下标位置,赋值
3、遍历完后,将新数组赋值给table对象

4、常见面试问题

4.1、为什么最大容量是2的32次方

在这里插入图片描述
如上图:当元素个数大于阈值threshold时,就会调用resize()进行扩容。但是当老表容量大于等于MAXIMUM_CAPACITY = 1 << 30,阈值则会被设置为Integer.MAX_VALUE后返回,不会再继续扩容。

4.2、为什么HashMap是线程不安全的,而HashTable与ConcurrentHashMap是线程安全的

在这里插入图片描述

从jdk1.7与1.8的源码中可以看出HashMap没有加锁,并且put()操作过程中当传入的hash和key值在数组中存在相等的不为空的,会出现值覆盖。如上图:假定k1与k2键值对完全相同,在多线程环境下进行put()操作,当k1、k2同时在1号位置并执行完成,可能会造成数据不一致。另一方面是在多线程扩容的情况下使用头插法导致循环链表问题。故,HashMap是线程不安全。
在这里插入图片描述
如上图:在Hashtable的源码中可以发现通过synchronized进行了加锁操作。同样k1,k2在多线程同一位置环境下执行,不论哪个先执行,后执行的一个必须要获取到先执行所释放的锁。故,Hashtable是线程安全的。
在这里插入图片描述
ConcurrentHashMap 是有segment数组和HashEntry组成的。如上图:源码中,segment继承ReentrantLock(可重入锁),保证了线程安全。在1.8中进行了锁优化(putVal()为例,如下图),采用synchronized +cas进行加锁保证线程的并发安全。故,ConcurrentHashMap 是线程安全的。
在这里插入图片描述

4.3、为什么说jdk1.7的头插法会导致循环链表问题、1.8的尾插法会可能数据丢失

头插法:
在这里插入图片描述
在这里插入图片描述
尾插法:
当多线程同时插入,数据可能会被覆盖从而导致数据丢失。

4.4、在jdk1.7中,是不是当数组长度超过阈值8就一定会转为红黑树结构?

不是。请看下面源码:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //判断tab为空 或者 tab的长度小于转红黑树tab最小长度,满足其中的任一条件执行的是扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //否则,tab对应下标位置不为空才开始树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

5、与HashTable、Concurrent HashMap的简略对比

对比图片

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值