深入理解hashmap

本文详细解析HashMap的非线程安全特性、快速访问与哈希冲突处理,介绍其内部数组+链表+红黑树结构,重点讲解哈希算法、索引定位与扩容策略,以及如何通过Collections和ConcurrentHashMap实现线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入理解hashmap

一,特点:

1 :非线程安全 (注:如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap)

2:有很快的访问速度,但遍历顺序却是不确定的

3:HashMap最多只允许一条记录的键为null,但是允许多条记录的值为nulll

二,内部实现:
2.1 从结构上来讲,hashmap 是数组 + 链表 + 红黑树 ( 在jdk1.8 中增加了红黑树部分) 进行实现的

img

看 hashmap 的源码

static class Node<K,V> implements Map.Entry<K,V> {
   
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
   
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

可以看到定义了Node<K , V> 上面每个小黑点就是 hashmap 的一个内部类。

2.2 hashmap 通过hash 表来进行存储, 但是hash表本身因为得出的hash 值可能相同,会造成hash 冲突,哈希表为了解决这个冲突,一般能够采用两种解决方法:开放地址法 和 链地址法。 在java 中使用了链地址法解决冲突,其实也就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上

比如

map.put("皮卡丘", "小智");

系统会算出"皮卡丘" 这个key 的 hashCode 方法,得到 hash 值,后再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。

当然,若是哈希桶的数组很大,这样产生碰撞的概率就低,hashmap 查找到对应数据的时间成本 就越低,但是这牺牲了 本就不多的内存空间,占用了空间成本。所以, 我们要保证这两者的平衡,采用好的hash算法 和 良好的扩容机制保证时间和空间的有效利用。

来看看源码中默认构造函数的初始参数


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

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * 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;

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

    /* ---------------- Public operations -------------- */

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

了解了hashmap 的基础概念后要注意两个注意点:

1: 在Hashmap 中 , 数组的长度必须是 2的n 次方, 就算我们手动输入一个非2 的n 次方的数,hashmap 会自动将其转为大于这个数的最接近的2的n 次方的数。 至于为啥呢? 接下来会有解释, 在这里先给一个概念: 为了在定位 哈希桶索引位置时做出的选择。

2:我们也发现了,尽管hash 算法设计得再合理, 也免不了产生hash 冲突的情况, 这造成了数据不断在同一个索引位置堆积( 以链表的形式) , 链表过长时,就会严重的影响hash map 的性能, 因此 在jdk 1.8 中,为了提高增删改查的性能 ,引入了红黑树 (当链表长度大于8时)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYcn5v24-1610716659849)(C:\Users\86150\AppData\Roaming\Typora\typora-user-images\image-20210115174707502.png)]

当红黑树的节点数目小于 6 时, 转为链表。

本文主要从根据key获取哈希桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解

一:哈希数组的索引位置

​ 首先 是hashmap的 初始化, 如果new hashmap( ) 中不传值, 默认大小 则是 16 , 负载因子 是 0.75 , 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。 那么这个是怎么实现的 呢?(不妨 看看源码)

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) 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值