HashMap详解

HashMap

引言

哈希表(HashTable)
也称散列表,是一种非常重要的数据结构,应用场景机器丰富,许多缓存技术的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK7的HashMap袁爱民进行分析

什么是哈希表

首先来了解下其他的数据结构再讲

  • 数组
    采用一段连续的存储单元来存储数据.对于制定下标的查找,时间复杂度为O(1),通过给定值进行查找,需要遍历数组,注意对比给定关键字和数组元素,时间复杂度为O(n),当然对于有序数组,正则可以采用二分查找,插值查找,斐波那契查找等方式,可将复杂度提高到O(logn)
  • 线性链表:
    对于链表新增,删除等操作(在找到指定操作位置后,仅需要处理节点之间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
  • 二叉树:
    对一棵数相对平和有序的二叉树,对其进行插入,查找,删除等操作,平均复杂度为O(logn)
  • 哈希表:
    相对上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突情况下,仅需一次定位即可完成,时间复杂度为O(1)接下啦Hash表是如何时间复杂度最低的

Tip:
数组简单搜下标快,搜元素慢
线性链表增删快,查找慢
二叉树都差不多
哈希表增删查都快

我们知道数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构,而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可达到,哈希表利用了这种特性,哈希表的主干就是数组.

比如我们要新增或查找某个元素,我们通过吧当前元素的关键字通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作
这个函数可以简单抽象为:cunchuweizhi =f(关键字),这个函数f一般称为hash函数,函数的好坏直接影响Hash表的优劣,如下执行插入数据

在这里插入图片描述

查找的时候也是同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址去除即可

哈希冲突

然而Hash并不是完美的.如果两个不同的元素通过Hash函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的hash冲突,也叫哈希碰撞,前面我们提到过,哈希函数的设计至关重要,好的函数会尽可能的保证计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数不能保证得到的存储地址绝不发生冲突.

那么Hash冲突如何解决呢?哈希冲突解决方案有很多中,开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数算法,而HashMap即采用了链地址法,也就是数组+链表的方式.

HashMap的实现原理

HashMap的主干是一个Entry数组.Entry是HashMap的基本组成单元.每一个Entry包含一个key-value键值对.其实所谓Map其实就是保存了两个对象之间映射关系的一种集合.
HashMap中Entry相关代码

//HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂。
//至于为什么这么做,后面会有详细分析。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry是hashMap中的一个静态内部类.代码如下

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

下图表示Entry是怎样存在的

简单来说,HashMap由数组+链表组成的,数组是HashMap的主题,链表则主要为了解决哈希冲突存在的,如果定位到的数组位置不含链表(如当前Entry的下一个指向的是null)那么查找,添加等操作很快,仅需一次寻址即可;如果定位到数组包含链表,对于添加操作,其复杂度为O(n)首先遍历表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过对key对象的equals方法注意比对查找.所以,性能考虑,HashMap中的链表出现越少,性能才会越好

其他几个重要字段

/**实际存储的key-value键值对的个数*/
transient int size;

/**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/
int threshold;

/**负载因子,代表了table的填充度有多少,默认是0.75
加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;

/**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException*/
transient int modCount;

HashMap的数组长度一定是2的次幂

重写equals方法须同时重写hashcode方法

JDK1.8中HashMap的性能优化

假如一个数组槽位上链上数据过多导致性能下降怎么办?
1.8在1.7的基础上针对增加了红黑树进行优化.即当链表超过8是,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入,删除,查找等算法.

总结(重点)

HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生产一个数字作为数组下标,这个数字就是hashcode)所以在查询上的访问速度比较快,HashMap最多运行一对键值对的Key为NULL,允许多对键值对的value为Null.他是非线程安全的,在排序上是无序的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值