HashMap原理

java HashMap 详解

了解HashMap之前,先来了解下hash算法

Hash算法

  按我的理解,hash算法相当于对一个值进行某种运算,可以生成一个与这个值对应的“唯一”标识,这个生成的过程是可预估时间的,但是逆向这个过程较为困难,且很难找到不同值生成的两个标识相同,这个过程就叫做hash算法。

  例如,有一万个文件,你想确认一个文件在不在一万个文件里,一一比对每个文件内容速度会很慢,如果对每个文件根据内容都生成一个唯一标识,那么只要比对这个标识是否重复就可以判断出来了。

  hash算法在HashMap中的主要应用就是针对key生成一个唯一值,尽可能的让键值对分配到不同的桶中

HashMap 是什么

HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap 是非线程安全的。

HashMap 是数组和链表的结合体,发生碰撞时,在该位置加一个链表。数据结构如下:

通过查看HashMap源码,在构造函数中,会创建一个Entry数组,Entry结构如下:

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

Entry 是一个 static class,其中包含了 key 和 value,也就是键值对,另外还包含了一个 next 的 Entry 指针。我们可以总结出:Entry 就是数组中的元素,每个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。

 

HashMap 的存储机制

当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

hash(int h)方法根据 key 的 hashCode 重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的 hash 冲突。

根据计算出的hash值分别存入不同的桶中,如果计算出的hash值相同,意味着发生碰撞,当我们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 重新计算 hash 值,根据 hash 值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

举个例子,有两个桶,一个装的是水果,一个装的是蔬菜,你要找到特定的水果或者蔬菜,要先找到对应的桶,再去桶里逐个寻找你要的水果或者蔬菜,但是分类更细的话,每种水果和每种蔬菜都各占一个桶,你只要找对应的桶就可以找到你要的,是一种拿空间换时间的做法。至于多少个桶,没有固定的要求。

HashMap的长度为什么是2的n次方

有两点原因

1. 通过查看源码,计算某个键值对应该保存的位置的时候时,使用的indexFor方法代码如下:

/**
     * Returns index for hash code h.
     */
static int indexFor(int h, int length) {  
    return h & (length-1);
}

使用的  & 即按位与运算

假设数组长度分别为 15 和 16,优化后的 hash 码分别为 8 和 9,那么 & 运算后的结果如下:

h & (table.length-1)hash table.length-1 
8 & (15-1):0100&1110= 0100
9 & (15-1):0101&1110= 0100
8 & (16-1):0100&1111= 0100
9 & (16-1):0101&1111= 0101

2n-1 得到的二进制数的每个位上的值都为 1,这使得在低位上&时,得到的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上形成链表。而15的后一位为0,导致数组空间浪费,而且增加碰撞几率,使得查询效率变低。

所以说当数组长度为2的次幂的时候,不同的key hash运算得到的结果相同的几率很小,使得数组分布较为均匀,碰撞的概率也变小了,从而查询的时候无需遍历数组某个位置的链表,提升查询效率。

2.长度为2的n次幂,可以使用效率较高的位移运算符 ">>"

HashMap 的性能参数

当HashMap中的数据越来越多时,发生碰撞的概率也越来越搞,这个时候会对数据进行扩容,默认长度为16,负载因子为0.75,也就是数据数目达到两者乘积也就是16*0.75=12时,数组会扩容为原来的两倍,2*16=32。这个扩容操作叫做rehash,因为要对原先所有数据重新计算位置,再放入扩容后的数组中,所以如果可以确定数组长度的话,提前设置好长度,可以减少性能消耗。

HashMap 包含如下几个构造器:

  • HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

HashMap 的基础构造器 HashMap(int initialCapacity, float loadFactor) 带有两个参数,它们是初始容量 initialCapacity 和负载因子 loadFactor。

负载因子 loadFactor 衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

HashMap 的实现中,通过 threshold 字段来判断 HashMap 的最大容量:

        threshold = (int)(capacity * loadFactor);

结合负载因子的定义公式可知,threshold 就是在此 loadFactor 和 capacity 对应下允许的最大元素数目,超过这个数目就重新 resize,以降低实际的负载因子。默认的的负载因子 0.75 是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize 后的 HashMap 容量是容量的两倍:

HashMap是否是线程安全的

HashMap是非线程安全的,当对其进行遍历的时候,如果其他线程修改了map,那么会报ConcurrentModificationException,这个是通过modCount 域实现的,类似计数器,迭代器初始化的时候会得到这个值,每次迭代去比较值是否发生了变化,如果有变化,则抛出异常,这个机制叫fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,因为 JDK 并不保证 fail-fast 机制一定会发生。若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。

ConcurrentHashMap

 

本篇内容主要参考该文章

http://wiki.jikexueyuan.com/project/java-collection/hashmap.html 

针对性的对部分内容举例说明

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值