Java 容器 - 一文详解HashMap

本文详细介绍了Java中的HashMap,包括其基于哈希表的实现、存储结构、重要属性如loadFactor和threshold,以及扩容机制。文章还探讨了hashCode()的作用、哈希碰撞的处理、Java 8引入红黑树的原因,以及HashMap与Hashtable、ConcurrentHashMap的差异。通过理解这些概念,有助于优化使用HashMap的效率和避免潜在问题。
摘要由CSDN通过智能技术生成

Map 类集合

Java Map类集合,与Collections类集合存在很大不同。它是与Collection 类平级的一个接口。

在集合框架中,通过部分视图方法这一根 微弱的线联系起来。

(在之后的分享中,我们会讨论到Collections 框架的内容)

Map类集合中的存储单位是K-V键值对,就是 使用一定的哈希算法形成一组比较均匀的哈希值作为Key,Value值挂在Key上。

Map类 的特点:

  • 没有重复的Key,可以具有多个重复的Value

  • Value可以是List/Map/Set对象

  • KV是否允许为null,以实现类的约束为准

Map集合类 Key Value Super JDK 说明
Hashtable 不允许为 null 不允许为 null Dictionary 1.0 (过时)线程安全类
ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 1.5 锁分段技术或CAS(JDK8 及以上)
TreeMap 不允许为 null 允许为 null AbstractMap 1.2 线程不安全(有序)
HashMap 允许为 null 允许为 null AbstractMap 1.2 线程不安全(resize 死链问题)

从jdk1.0-1.5,这几个重点KV集合类,见证了Java语言成为工业级语言的成长历程。

知识点:

  1. Map 类 特有的三个方法是keySet()values()entrySet(),其中values()方法返回的视图的集合实现类是Values extends AbstractCollection<V>,没有实现add操作,实现了remove/clear等相关操作,调用add方法时会抛出异常。
  2. 在大多数情况下,直接使用ConcurrentHashMap替代HashMap没有任何问题,性能上面差别不大,且线程安全。
  3. 任何Map类集合中,都要尽量避免KV设置为null值。
  4. Hashtable - HashMap - ConcurrentHashMap 之间的关系 大致相当于 Vector - ArrayList - CopyOnWriteArrayList 之间的关系,当然HashMap 和 ConcurrentHashMap之间性能差距更小。

一、hashCode()

哈希算法 哈希值

在Object 类中,hashCode()方法是一个被native修饰的类,JavaDoc中描述的是返回该对象的哈希值。

那么哈希值这个返回值是有什么作用呢?

主要是保证基于散列的集合,如HashSet、HashMap以及HashTable等,在插入元素时保证元素不可重复,同时为了提高元素的插入删除便利效率而设计;主要是为了查找的便捷性而存在。

拿Set进行举例,

众所周知,Set集合是不能重复,如果每次添加数据都拿新元素去和集合内部元素进行逐一地equal()比较,那么插入十万条数据的效率可以说是非常低的。

所以在添加数据的时候就出现了哈希表的应用,哈希算法也称之为散列算法,当添加一个值的时候,先去计算出它的哈希值,根据算出的哈希值将数据插入指定位置。这样的话就避免了一直去使用equal()比较的效率问题。

具体表现在:

  • 如果指定位置为空,则直接添加
  • 如果指定位置不为空,调用equal() 判断两个元素是否相同,如果相同则不存储

上述第二种情况中,如果两个元素不相同,但是hashCode()相同,那就是发生了我们所谓的哈希碰撞。

哈希碰撞的概率取决于hashCode()计算方式和空间容量的大小。

这种情况下,会在相同的位置,创建一个链表,把key值相同的元素存放到链表中。

在HashMap中就是使用拉链法来解决hashCode冲突。

总结

hashCode是一个对象的标识,Java中对象的hashCode是一个int类型值。通过hashCode来指定数组的索引可以快速定位到要找的对象在数组中的位置,之后再遍历链表找到对应值,理想情况下时间复杂度为O(1),并且不同对象可以拥有相同的hashCode。

HashMap 底层实现

带着问题

  1. HashMap 的长度为什么默认初始长度是16,并且每次resize()的时候,长度必须是2的幂次方?
  2. 你熟悉HashMap的扩容机制吗?
  3. 你熟悉HashMap的死链问题吗?
  4. Java 7 和 Java 8 HashMap有哪些差别?
  5. 为什么Java 8之后,HashMap、ConcurrentHashMap要引入红黑树?

0. 简介

  1. HashMap 基于哈希表的Map接口实现的,是以Key-Value存储形式存在;
  2. 非线程安全;
  3. key value都可以为null;
  4. HashMap中的映射不是有序的;
  5. 在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构;
  6. 当一个哈希桶存储的链表长度大于8 会将链表转换成红黑树,小于6时则从红黑树转换成链表;
  7. 1.8之前和1.8及以后的源码,差别较大

1. 存储结构

在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构。

通过哈希来确认到数组的位置,如果发生哈希碰撞就以链表的形式存储 ,但是这样如果链表过长来的话,HashMap会把这个链表转换成红黑树来存储,阈值为8。

下面是HashMap的结构图:

2. 重要属性

2.1 table
  	/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组。

2.2 size
    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

HashMap中 键值对存储数量。

2.3 loadFactor
    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

负载因子。负载因子是权衡资源利用率与分配空间的系数。当元素总量 > 数组长度 * 负载因子 时会进行扩容操作。

2.4 threshold
    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

扩容阈值。threshold = 数组长度 * 负载因子。超过后执行扩容操作。

2.5 TREEIFY_THRESHOLD/UNTREEIFY_THRESHOLD
    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

树形化阈值。当一个哈希桶存储的链表长度大于8 会将链表转换成红黑树,小于6时则从红黑树转换成链表。

3. 增加元素

    public V put(K key, V value) {
   
        return putVal(hash(key), key, value, false, true);
    }
3.1 hash()

可以看到实际执行添加元素的是putVal()操作,在执行putVal()之前,先是对key执行了hash()方法,让我们看下里面做了什么

    static final int hash(Object key) {
   
        int h;
		// key.hashCode():返回散列值也就是hashcode
		// ^ :按位异或
		// >>>:无符号右移,忽略符号位,空位都以0补齐
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

key==null说明,HashMap中是支持key为null的情况的。

同样的方法在Hashstable中是直接用key来获取hashCode,没有key==null的判断,所以Hashstable是不支持key为null的。

再回来说这个hash()方法。这个方法用专业术语来称呼就叫做扰动函数

使用hash()也就是扰动函数,是为了防止一些实现比较差的hashCode()方法。换句话来说,就是为了减少哈希碰撞

JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。我们再看下JDK1.7中是怎么做的。

        // code in JDK1.7
		static int hash(int h) 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值