博客
关于我
java基础--java中HashMap原理
阅读量:392 次
发布时间:2019-03-05

本文共 3881 字,大约阅读时间需要 12 分钟。

HashMap的工作原理及优化

1、为什么使用HashMap?

HashMap是一种基于散列(Hashing)的数据结构,适用于快速的键值对存储与检索。其主要优势在于:

  • 高效性:HashMap采用数组和链表结合的存储方式,能够在平均情况下O(1)时间复杂度内完成插入、删除和查找操作。
  • 非线程安全:HashMap默认不支持线程安全,适用于单线程或不需要并发控制的场景。
  • 灵活性:HashMap允许存储null键和null值,提供了更大的灵活性。

与Hashtable相比,HashMap的优势体现在:

  • 性能优化:HashMap通过哈希冲突率更低和链表结构优化,实现了比Hashtable更高效的操作。
  • 更灵活的null支持:HashMap允许null键和null值,而Hashtable不支持。

2、HashMap的工作原理

2.1、存储结构

HashMap以数组为核心存储结构,每个数组元素称为桶(Bucket),每个桶可以存储一个或多个键值对。键值对由节点(Node)组成,每个节点包含以下信息:

  • 键(key):存储键对象的引用。
  • 值(value):存储值对象的引用。
  • 哈希值(hash):存储键的哈希值。
  • 指针(next):指向下一个节点,用于链表连接。

2.2、哈希计算

HashMap中的哈希函数用于计算键的哈希值,从而确定键在数组中的位置。JDK8版本的哈希函数实现如下:

static final int hash(Object key) {    if (key == null) {        return 0;    }    int h = key.hashCode();    h = h ^ (h >>> 16);    return h & (capacity - 1);}

哈希函数的作用是:

  • 高效性:通过按位异或和右移操作,减少哈希冲突。
  • 分布均匀:通过对哈希值取模操作,确保键分布尽可能均匀。
  • 2.3、存储与链表

    当插入键值对时,首先计算键的哈希值,确定对应的桶位置。如果该位置为空,直接存入;如果存在哈希冲突,链表形式逐个检查,直到找到空槽或替换旧值。

    2.4、链表与红黑树

    当链表长度超过8时,转换为红黑树以减少查询深度;链表长度小于6时,转回链表以优化性能。

    3、减少哈希冲突的方法

    3.1、扰动函数

    哈希冲突的主要原因是不同的键可能有相同的哈希值。扰动函数通过增强哈希函数的混乱度,减少冲突概率。例如,JDK8的哈希函数采用了高低位异或和右移操作。

    3.2、对象不可变性

    确保键对象不可变,并重写equalshashCode方法。不可变性确保哈希值一致性,避免因对象状态变化导致哈希值不匹配。

    3.3、合理选择键类型

    选择不可变的对象(如String、Integer)作为键,减少哈希冲突。这些对象已预定义了equalshashCode方法,保证一致性。

    4、HashMap的哈希函数实现

    4.1、哈希函数的核心逻辑

    哈希函数的关键在于高效计算和分布均匀。JDK8版本的哈希函数通过高低位异或和右移操作,减少冲突。具体实现如下:

  • 高16位:与高16位异或,减少冲突。
  • 低16位:与低16位右移16位,提升混乱度。
  • 取模:对哈希值取模,确保在数组范围内。
  • 4.2、实际应用

    在实际应用中,哈希函数的选择至关重要。优化好的哈希函数能显著提升性能,减少碰撞率。

    5、链表过深问题与红黑树

    5.1、链表过深

    当链表长度超过8时,查询深度会显著增加,影响性能。因此,转换为红黑树以平衡查询深度。

    5.2、红黑树优缺点

    红黑树的插入和删除操作需要额外的旋转操作,但查询深度仅为log级别,优于链表。

    6、哈希碰撞的解决方法

    6.1、开放定址法

    当哈希冲突发生时,采用探查序列逐个检查,直到找到空槽。常见探查方法包括线性探查和二次探查。

    6.2、双重哈希法

    通过使用双哈希函数,降低碰撞概率。例如,计算两个不同哈希值,只有两者都冲突时才认为是碰撞。

    7、HashMap容量扩展

    7.1、负载因子

    默认负载因子为0.75。当容量达到75%时,触发扩容。新容量为原容量的两倍,重新计算哈希值并迁移数据。

    7.2、扩容过程

  • 扩容:创建新数组,大小为原容量的两倍。
  • 迁移:将旧数组中的数据逐一迁移到新数组,确保数据一致性。
  • 链表顺序:新数组中的链表顺序与旧数组保持一致,避免尾部遍历。
  • 8、线程安全与扩容问题

    8.1、条件竞争

    在扩容过程中,多线程可能导致数据不一致。解决方法是确保扩容操作的线性时间内只允许一个线程进行。

    8.2、链表反转

    扩容时,链表的顺序可能发生反转,避免尾部遍历带来的性能问题。

    9、HashMap的扩容源代码解析

    9.1、put方法

    public V put(K key, V value) {    // 计算哈希值    int hash = hash(key);    int i = indexFor(hash, table.length);    // 替换旧值或新增节点    for (Entry e = table[i]; e != null; e = e.next) {        if (e.hash == hash && e.key.equals(key)) {            e.value = value;            return e.value;        }    }    // 增加计数    modCount++;    // 添加新节点    addEntry(hash, key, value, i);    return null;}

    9.2、addEntry方法

    void addEntry(int hash, K key, V value, int bucketIndex) {    Entry e = table[bucketIndex];    table[bucketIndex] = new Entry(hash, key, value, e);    // 检查容量是否需要扩容    if (size++ >= threshold) {        resize(2 * table.length);    }}

    9.3、resize方法

    void resize(int newCapacity) {    Entry[] oldTable = table;    // 创建新数组    int oldCapacity = oldTable.length;    Entry[] newTable = new Entry[newCapacity];    // 迁移数据    transfer(newTable);    // 更新容量和阈值    table = newTable;    threshold = (int)(newCapacity * loadFactor);}

    9.4、transfer方法

    void transfer(Entry[] newTable) {    Entry[] src = table;    int newCapacity = newTable.length;    for (int j = 0; j < src.length; j++) {        Entry e = src[j];        if (e != null) {            src[j] = null;            do {                Entry next = e.next;                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            } while (e != null);        }    }}

    10、线程安全容器的选择

    10.1、Hashtable

    Hashtable是线程安全的,但性能较低。所有方法都带有sync关键字,实现方法层次的同步。

    10.2、ConcurrentHashMap

    ConcurrentHashMap采用锁分离技术,通过多个锁管理不同段,支持多个修改操作,性能优于Hashtable。

    10.3、CopyOnWriteArrayList

    基于写时复制机制,读操作不需要锁。适用于读操作多于写操作的场景,但内存占用较高。

    10.4、Vector

    Vector是线程安全的随机访问容器,但性能较差,已被较少使用。

    11、StringBuffer与StringBuilder

    11.1、StringBuffer

    StringBuffer是线程安全的,支持多线程并发操作,但性能较低。

    11.2、StringBuilder

    StringBuilder是不线程安全的,性能优于StringBuffer,适用于单线程或不需要并发控制的场景。

    总结

    HashMap作为Java中最常用的散列表,其优点在于高效性和灵活性。通过优化哈希函数、处理链表与红黑树、解决哈希碰撞和容量扩展,可以进一步提升HashMap的性能。在并发环境下,选择适合的线程安全容器至关重要。

    转载地址:http://wtczz.baihongyu.com/

    你可能感兴趣的文章