HashMap的底层原理

本文详细解析了HashMap的工作原理,包括其初始容量、填充因子、扩容机制、哈希算法、存储与检索流程,以及链表和红黑树转换条件,是理解Java集合框架中HashMap不可或缺的指南。

一.特点:

HashMap的初始容量为16,填充因子为0.75.扩展容量是当前容量的2倍.
当要存入的数据量大于16*0.75(即大于12)时,底层数组会发生扩容,存储的数据会重新计算hashcode,链表及红黑树也会发生变化

※※※※※※※※快速扩容的方法※※※※※※※:
设置HashMap底层数组长度为:
要存入的数据量/0.75<=2的n次方,(取n的最小值)
例:
1000个值,则需设置底层数组的长度为1000/0.75<=2的n次方
结果为:16,即需设置初始长度为1024


二.HashMap的哈希算法:

  • 1.得到key值的hashcode
  • 2.对hashcode值进行异或运算
  • 3.对异或的结果和初始容量(即数组大小)-1 做&运算
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
i = (n - 1) & hash

这样做的目的:
当数组容量很小的时候,计算元素在数组中的位置(n-1)&hash,只用到了hash值的低位,这样当不同的hash值低位相同,高位不同的时候会产生冲突。实际上的hash值将hashcode低16位与高16位做异或运算,相当于混合了高位和低位,增加了随机性。当然是冲突越少越好,元素的分布越随机越好。


三.HashMap存取元素(哈希表)

HashMap初始数组长度为16。数组的每个元素都保存着链表头的地址(或者为null),在向HashMap中put(key,value)的时候,先使用hash算法计算哈希值,然后再和数组的长度减一做与运算。计算出此键值对应该保存到数组的那个位置上,如果此位置没有元素,意思就是链表的头结点为null,那么就新建一个node结点,把key,value以及next保存。

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结点(即Node<K,V>)是一个实现了Map.Entry的内部类,里面的属性有hash值,keyvalue,以及next
当执行put(key,value)的时候,如果计算出来的数组位置上有元素的话(说明计算出的hash值和此数组位置对应的单链表上的所有元素的hash值相同,即发生冲突。),就沿着此数组位置对应的单链表上的结点一个个比较(使用equals()方法),如果遇到相同的key,就用新的value替换掉旧的value,如果找不到相同的key,就新建一个node结点,保存hash值,key和value,然后插入到此单链表的尾部。插入之后,这里程序会判断此单链表上的结点个数(这里注意,不是全部的元素结点个数,而是此单链表上的结点个数,和其他数组位置上的单链表无关)是否超过限制(HashMap默认是8),如果超过限制,那么HashMap就会把此单链表转成红黑树,这样做的目的是为了提高get(key)的速度。由时间复杂度原来的O(n)变成了O(logn)。到这还不算完,一旦插入新的node,程序就会检查HashMap的装载量(全部键值对的个数)是否超过阈值,这个阈值是计算出来的,就是装载因子乘上数组容量。一旦装载量大于此阈值,程序就会执行resize()方法进行扩展容量,HashMap是直接扩容2倍,扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率,但是扩容是很费时的。

在这里插入图片描述


四.源码:

1.put()方法

在这里插入图片描述

get()方法

在这里插入图片描述

### HashMap 底层数据结构 HashMap底层结构采用 **数组 + 链表/红黑树** 的方式实现。这种结构结合了数组的快速访问特性和链表/红黑树在冲突处理中的高效性。HashMap 通过哈希函数将键(key)转换为哈希值,然后根据哈希值计算出键值对在数组中的存储位置(索引)[^2]。 - **数组**:用于存储数据的主结构,数组的每个元素被称为“桶”(bucket)。 - **链表**:当多个键值对被映射到同一个桶时,使用链表来存储这些冲突的键值对。 - **红黑树**:当某个桶中的链表长度超过一定阈值(默认为8),链表会转换为红黑树以提升查找效率[^2]。 ### HashMap 的实现原理 HashMap 的核心实现原理围绕 **哈希算法** 和 **冲突解决机制** 展开: 1. **哈希算法**:HashMap 使用键的 `hashCode()` 方法结合扰动函数生成最终的哈希值。通过 `(n - 1) & hash` 计算出键值对的存储位置,其中 `n` 是数组的长度。这种方式确保哈希值均匀分布,减少碰撞概率[^5]。 2. **冲突解决**:当两个不同的键经过哈希计算后映射到相同的桶时,会发生哈希冲突。HashMap 使用 **拉链法** 解决冲突,即在每个桶中维护一个链表,存储所有冲突的键值对。当链表长度超过阈值时,链表会转换为红黑树以提升性能。 3. **动态扩容**:当 HashMap 中的元素数量接近其容量与负载因子(默认为0.75)的乘积时,HashMap 会自动扩容(通常是当前容量的两倍),并重新分配所有键值对到新的桶中。这个过程称为 **再哈希**(rehash)[^5]。 4. **线程安全性**:HashMap 不是线程安全的,在多线程环境下可能会导致数据不一致或死循环等问题。如果需要线程安全的实现,可以使用 `ConcurrentHashMap`[^3]。 ### 示例代码:HashMap 的基本使用 ```java import java.util.HashMap; public class HashMapExample { public static void main(String[] args) { // 创建一个 HashMap 实例 HashMap<String, Integer> map = new HashMap<>(); // 添加键值对 map.put("Apple", 10); map.put("Banana", 20); map.put("Orange", 30); // 获取值 System.out.println("Apple: " + map.get("Apple")); // 输出 Apple: 10 // 遍历 HashMap for (String key : map.keySet()) { System.out.println(key + ": " + map.get(key)); } // 删除键值对 map.remove("Banana"); // 检查是否包含某个键 if (map.containsKey("Orange")) { System.out.println("Contains Orange"); } } } ``` ### 性能优化与使用场景 HashMap 的性能优势主要体现在 **快速的插入、查找和删除操作**,其平均时间复杂度为 O(1)。这种高效性得益于哈希算法的均匀分布和冲突处理机制的优化(链表转红黑树)。 在实际开发中,HashMap 被广泛用于缓存、索引、快速查找等场景。例如,在金融领域中,HashMap 和 `ConcurrentHashMap` 被频繁用于处理高并发的数据存储和访问需求[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值