HashMap学习分析-20220324- 简单的属性和构造方法

1.HashMap简介

HashMap是基于hash表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。

HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
}

JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”,

阈值(边界值)>8并且数组长度大于64,才将链表转换为红黑树,将O(n) 时间复杂度提升至O(log n)。

当HashMap中的其中一个链表的对象个数如果TREEIFY_THRESHOLD达到了8个,此时如果数组长度没有达到64,那么 HashMap会先扩容解决,如果已经达到了了64,那么这个链表会变成红黑树,节点类型由Node变成 TreeNode类型。当然,如果映射关系被移除后,下次执行resize⽅法时判断树的节点个数低于6,也会再把树转换为链表。

2.HashMap的一些基本属性

    /**
     * 默认初始容量16,必须是2的幂
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 默认负载因子。当键值对的数量大于 CAPACITY * 0.75 时,就会触发扩容
     * 如无参的HashMap构造器,初始化容量为16,当键值对的数量大于 16 * 0.75 = 12时,就会触发扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 扩容阈值,链表的元素大于8的时候,链表将转化为树
     */
    static final int TREEIFY_THRESHOLD = 8;
    /**
     * 扩容阈值,resize操作时,红黑树的节点数量小于6时使用链表来代替树
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    /**
     * 在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。
     * 这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

 loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

threshold 除了用于存放扩容阈值还有其他作用吗?

在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费

3.HashMap的构造方法

HashMap的构造方法有4种,主要涉及到的参数有,指定初始容量,指定填充比和用来初始化的Map

//构造函数1
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);//新的扩容临界值
}
 
//构造函数2 创建HashMap时只传入初始化容量
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
 
//构造函数3 创建HashMap时不传入任何参数
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
 
//构造函数4用m的元素初始化散列映射 创建HashMap时传入一个实现了Map接口的对象,此HashMap创建后会包括此Map的所有内容
public HashMap(Map<!--? extends K, ? extends V--> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) {
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold) // 传入Map的size大于扩容容量,resize方法为重新初始化一下容量
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);// 相当于put方法
        }
    }
}

 ------------------------------------------------------------------------------------------------------------------------

涉及到该篇的一些面试题

1.HashMap 的默认初始容量是多少?为什么?HashMap 的容量有什么限制吗?

 默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

16的原因主要是:16是2的N次方,并且是一个较合理的大小。如果用8或32,我觉得也是OK的。实际上,我们在新建 HashMap 时,最好是根据自己使用情况设置初始容量,这才是最合理的方案。

2.负载因子为啥是0.75

这个也是在时间和空间上权衡的结果。如果值较高,例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;而如果值较低,例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折衷考虑 0.75 似乎是一个合理的值。 

——————————————————

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费

3.为什么要改成“数组+链表+红黑树”?

主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。 

4.为什么链表转红黑树的阈值是8?

我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果(这 B 我装定了)。

红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。

理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006(跟大乐透一等奖差不多,中大乐透?不存在的),这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。
 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值