HashMap源码阅读启读

上一篇文章让是我对整个java集合的基础认识,让我能够使用集合,下面我将对其中几个重要集合的(ArrayList,LinkList,HashMap)源码进行阅读。

一、HashMap是什么?

弄清楚这点!!我觉得这是最重要的一点。
官方的解释是:

HashMap是基于哈希表的实现的Map接口。此实现提供了所有可选的Map操作,并允许null的值和null键。 ( HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null)。这个类不能保证Map的顺序; 特别是,它不能保证订单在一段时间内保持不变。

为什么要用Hash

我了解了下为什么要用hashcode计算存入的键:
因为我们存入的键不能重复,那么我们怎么判断重复了,当然是用hashcode()方法把键转成hash码进行比较咯。为什么不直接比较呢,我的理解是,因为直接比较就跟肉眼去看两个人是否是双胞胎,而用hashcode计算后的哈希码比较就相当于把他们的DNA进行比较。
但别人是这样解释的:

也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。

哈希是什么

把任意长度的输入(输入叫做预映射,知道就行),通过一种函数(hashCode() 方法),变换成固定长度的输出,该输出就是哈希值(hashCode),这种函数就叫做哈希函数,而计算哈希值的过程就叫做哈希。哈希的主要应用是哈希表和分布式缓存。

哈希冲突又是什么:

哈希表选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是哈希冲突(哈希碰撞)。
解决哈希冲突 有两种方法,拉链法(链接法)和开放定址法。拉链法就是:将键值对对象封装为一个node结点(数组中的一个值),新增了next指向,这样就可以将碰撞的结点链接成一条单链表,保存在该地址(数组位置)中。

HashMap中并不是直接用的hashcode产生的哈希码,而是进行了一些位计算,但并未改变结果,至于为什么这么做 请参考这篇博客:HashMap中的hash算法中的几个疑问 总之就是为了提高性能

HashMap中的hash函数的源码如下:

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

二、HashMap的存储结构:

网上这张图片即表示了运用了拉链法的HashMap的存储结构(也叫桶数组):
当存在相同hashcode的key的时候,把他插入后面的链表,使用的是头插法(因为HashMap的发明者认为,后插入的key被查找的可能性更大)。
在这里插入图片描述HashMap的这种特殊存储结构在获取指定元素前需要把key经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率极高。
图中数组的索引= HashCode(Key)%length //length为HashMap的长度

均匀分布提高效率?

均匀分布其实是为了减少hash碰撞,因为减少了碰撞,效率才会提升。
哈希表长度越长,空间成本越大,哈希函数计算结果越分散均匀。哈希函数计算结果越分散均匀,哈希碰撞的概率就越小,map的存取效率(时间复杂度)就会越高。 因此我们的HashMap设计者进行了堪称完美的设计来提高效率。

实现均匀分布:
那么如何实现一个尽量均匀分布 的Hash函数呢?发明者们规定了HashMap的长度必须是2的幂次方,而后发现计算公式可演变为位运算:index = HashCode(Key) & (length - 1)
至于为什么能这样能均匀分布请参考:漫画讲解HashMap

看到一位大佬对使用HashMap的特点解释如下:下面我也将通过源码逐个讲解他们具体是如何通过代码实现的。

HashMap的出现是为了实现一种快速的查找并且插入、删除性能都不错的一种K/V(key/value)数据结构:

  • 为了实现快速查找,HashMap 选择了数组而不是链表。以利用数组的索引实现 O(1) 复杂度的查找效率。
  • 为了利用索引查找,HashMap引入 Hash 算法, 将 key 映射成数组下标: key -> Index。 引入 Hash 算法又导致了 Hash 冲突。
  • 为了解决Hash 冲突,HashMap 采用链地址法,在冲突位置转为使用链表存储。 链表存储过多的节点又导致了在链表上节点的查找性能的恶化。
  • 为了优化查找性能,HashMap 在链表长度超过 8 之后转而将链表转变成红黑树,以将 O(n) 复杂度的查找效率提升至 O(log n)。

三、HashMap.java

3.1、属性解释

下面是这个类中的属性解释:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8; 
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table; 
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器 例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化
    transient int modCount;   
    //临界值(最大node结点(键值对)容量)当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 填充因子
    final float loadFactor;
}

两个重点属性:

loadFactor加载因子:

loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

threshold:

threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

3.2、HashMap的扩容

扩容分为两种:
①创建时如果不指定容量初始值,HashMap默认的初始化大小为16,之后每次扩充,容量变为原来的2倍。(区别一下Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1)
②创建时如果给定了容量初始值, HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面详讲)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。(Hashtable 会直接使用你给定的大小)

3.3、解决hash冲突

我们知道解决hash冲突的原理就是,当发生碰撞时,就把相同hash值的 K-V对 组成链表。当链表长度大于阈值(即属性TREEIFY_THRESHOLD 默认为8)时,将链表转化为红黑树,以减少搜索时间。

/**这个方法即保证了 HashMap 总是使用2的幂作为哈希表的大小。
     * Returns a power of two size for the given target capacity.
     * 返回给定容量的两倍大小
     */
    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) ? MAXIMUM_CAPACITY : n + 1;
    }

3.4、构造方法

3.5、存数据-----put方法

3.6、取数据-----get方法

其他方法:
在这里插入图片描述

get,put方法参考

发布了44 篇原创文章 · 获赞 3 · 访问量 1372

猜你喜欢

转载自blog.csdn.net/weixin_43329639/article/details/103434608
今日推荐