HashMap

HashMap底层原理+常见面试问题

对于JAVA求职者来说,HashMap 可谓是重中之重,是面试必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大,库森当年校招面试时就经历过这种痛苦,结合自己的面试经验,斗胆写一篇关于 HashMap 的面试专题文章,希望对小伙伴有所帮助!

1. 存储结构

HashMap的底层数据结构是什么?

在JDK1.7 和JDK1.8 中有所差别:

在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表超过 8 且数据总量超过 64 才会转红黑树。

  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

Jdk1.8 HashMap结构

更深入的面试问题,

为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

不用红黑树,用二叉查找树可以么?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

为什么链表改为红黑树的阈值是 8?

是因为泊松分布,我们来看作者在源码中的注释:

 Because TreeNodes are about twice the size of regular nodes, we
 use them only when bins contain enough nodes to warrant use
 (see TREEIFY_THRESHOLD). And when they become too small (due to
 removal or resizing) they are converted back to plain bins.  In
 usages with well-distributed user hashCodes, tree bins are
 rarely used.  Ideally, under random hashCodes, the frequency of
 nodes in bins follows a Poisson distribution
 (http://en.wikipedia.org/wiki/Poisson_distribution) with a
 parameter of about 0.5 on average for the default resizing
 threshold of 0.75, although with a large variance because of
 resizing granularity. Ignoring variance, the expected
 occurrences of list size k are (exp(-0.5) pow(0.5, k) /
 factorial(k)). The first values are:
 0:    0.60653066
 1:    0.30326533
 2:    0.07581633
 3:    0.01263606
 4:    0.00157952
 5:    0.00015795
 6:    0.00001316
 7:    0.00000094
 8:    0.00000006
 more: less than 1 in ten million

翻译过来大概的意思是:理想情况下使用随机的哈希码,哈希冲突导致链表的长度频率遵循泊松分布, 各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率 ,可以看到链表中元素个数为 8 时的命中概率已经非常小,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

字段结构

默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

回答这个问题前,我们来先看下HashMap的默认构造函数:

     int threshold;             // 扩容阈值,当前哈希表中的元素超过阈值时候,出发扩容
     final float loadFactor;    // 负载因子
     int modCount;  //哈希表结构修改次数,添加和删除都算修改,替换不算
     int size;   //当前哈希表存储元素的个数
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认初始容量
     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;//链表超过8且存储的元素超过64链表才会转红黑树
           
	 transient Node<K,V>[] table;//哈希表结构Node类型的数组
     //Node的数据结构
	 static class Node<K,V> implements Map.Entry<K,V> {
   
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
     }

Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳键值对的最大值。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

默认的loadFactor是0.75,0.75是对空间利用率和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。

  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

我们来追溯下作者在源码中的注释(JDK1.7):

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

翻译过来大概的意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。

构造方法

public HashMap(int initialCapacity, float loadFactor) {
   
    //逻辑判断initialCapacity>0&&initialCapacity<=MAXIMUM_CAPACITY
    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;
    //返回第一个大于等于initialCapacity的2的2的n次方数
    this.threshold = tableSizeFor(initialCapacity);
}

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

2. 索引计算

HashMap 中 key 的存储索引是怎么计算的?

首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。看看源码的实现

同一个链表中的hash值相同吗?

不一定

// jdk1.7
方法一:
static int hash(int h) {
   
    int h = hashSeed;
        if (0 != h && k instanceof String) {
   
            return sun.misc.Hashing.stringHash32((String) k);
        }

    h ^= k.hashCode(); // 为第一步:取hashCode值
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4);
}
方法二:
static int indexFor
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值