HashMap原理及实现:变量、常量解析、hash算法解析、key的下标算法解析(一)

188 阅读5分钟

1. 前言

HashMap作为Java数据结构中重要的一员,同时拥有高效的查询和插入的优点。这篇文章主要分析HashMap的原理,以及如何自己实现一个HashMap

2. HashMap原理

2.1 什么是HashMap?

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ——摘自百度百科。

2.2 HashMap中的变量详解

2.2.1 table

存储key-value的数组,NodeHashMap的内部类,还有一个TreeNode
ps:Node是链表结构,TreeNode是红黑树结构

/**
 * 该表在首次使用时初始化,并根据需要调整大小。
 * 分配时,长度始终是 2 的幂。 
 *(我们还在某些操作中允许长度为零,以允许当前不需要的引导机制。)
 */
transient Node<K,V>[] table;

2.2.2 entrySet

/**
 * 保存缓存的 entrySet()。
 * 请注意,AbstractMap 字段用于 keySet() 和 values()。
 */
transient Set<Map.Entry<K,V>> entrySet;

2.2.3 size

/**
 * 当前hashMap的key-value的数量.
 */
transient int size;

2.2.4 modCount

了解即可,用于存储hashMap数据结构的修改次数

/**
 * 此 HashMap 已在结构上修改的次数 
 * 结构修改是更改 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新散列)的那些。
 * 该字段用于使 HashMap 的 Collection-views 上的迭代器快速失败。 
 *(请参阅 ConcurrentModificationException)。
 * ps:添加、修改、删除、清除都会加一次
 */
transient int modCount;

2.2.5 DEFAULT_LOAD_FACTOR loadFactor threshold

/**
 * 构造函数中未指定时使用的负载因子。
 * ps:当前数据量达到table.length的负载阈值时会进行table扩容
 * 例如:当前table的长度是16.当你key超过12个(16*0.75)就会触发HashMap扩容机制
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 哈希表的负载因子,这个是在new HashMap时可以更改的,如果没有传入则使用上面的默认值
 * @serial
 */
final float loadFactor;

/**
 * 具体的阈值,计算方式:容量(table.length)* 负载因子 (默认是0.75,可以在构建HashMap时更改)
 * @serial
 */
int threshold;

2.2.6 DEFAULT_INITIAL_CAPACITY

hashMap数组的初始值,默认是16

/**
 * 默认初始容量 - 必须是 2 的幂。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 当前默认是 16

2.2.7 MAXIMUM_CAPACITY

hashMap数组的最大值:1073741824

/**
 * 最大容量,如果一个更高的值由任何一个带参数的构造函数隐式指定时使用。必须是 2 <= 1<<30 的幂。
 * ps:当前HashMap能承载的最大容量,1<<30(1左移30位)的值是1073741824
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

2.2.8 TREEIFY_THRESHOLD

hashMap数组中单个链表最大长度限制,超过就会转换成树(红黑树结构)

/**
 * 使用树而不是列表的 bin 计数阈值。
 * 将元素添加到至少具有这么多节点的 bin 时,bin 将转换为树。
 * 该值必须大于 2 并且应该至少为 8,以便与树移除中关于在收缩时转换回普通 bin 的假设相吻合
 * ps:这是判断单个table[i]内的数据超过此阀值就会由链表转换为树结构,也就是常听说的红黑树
 */
static final int TREEIFY_THRESHOLD = 8;


2.2.9 UNTREEIFY_THRESHOLD

hashMap数组中单个树(由链表转换而来的)值小于6,就会转回链表结构

/**
 * 在调整大小操作期间 untreeifying(拆分)bin 的 bin 计数阈值。
 * 应小于 TREEIFY_THRESHOLD,并且最多 6 以在移除时进行收缩检测。
 * ps: 这是单个table[i]中的数据长度超过了8转换成了红黑树,
       但如果remove掉一些数据让长度小于此阈值(6)就会从树转换成正常的链表
 */
static final int UNTREEIFY_THRESHOLD = 6;


2.2.10 MIN_TREEIFY_CAPACITY

hashMap中单个Node转换成TreeNode并继续赋值时就会判断table的中长度,如果总长度小于MIN_TREEIFY_CAPACITY就会扩容一次

/**
 * 可对其进行树化的 bin 的最小表容量。
 *(否则,如果 bin 中有太多节点,则调整表的大小。)
 * 应至少为 4 * TREEIFY_THRESHOLD 以避免调整大小和树化阈值之间的冲突。
 * ps: 链表转换为树结构时,如果table.lenght长度小于64,就会强行扩容一次
 */
static final int MIN_TREEIFY_CAPACITY = 64;

2.3 HashMap中的内部类:Node和TreeNode

2.3.1 Node

/**
 * 基本哈希 bin 节点,用于大多数条目。 
 *(参见下面的 TreeNode 子类,以及 LinkedHashMap 中的 Entry 子类。)
 * ps:典型的链表存储
 */
static class Node<K,V> implements Map.Entry<K,V> {
    // 经过HashMap.hash()方法处理过的hash值
    final int hash;
    final K key;
    V value;
    // 当发生Hash冲突时会将冲突的数据存储到next
    Node<K,V> next;
    ...
}

2.4 什么是Hash

Hash是一种算法,能将一个任意长度的二进制值通过一个映射关系转换成一个固定长度的二进制值。在HashMap中,哈希算法主要是用于根据key的值算出存放数组的index值。HashMap中的存储都是根据key的哈希值有关的。
哈希算法又被称为散列的过程,那么什么是散列呢?散列即把数据均匀的存放到数组中的各个位置,从而尽量避免出现多个数据存放在一块区间内

优秀的哈希算法应该具备以下两点:

  • 保证散列值非常均匀
  • 保证冲突极少出现 下面是HashMap类中计算hash的源码:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

通过hashCode()方法,计算得到keyhash值(hash值本为32位的int类型)
key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。

h>>>16h >>> 16

表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。

hash.png
这样做的原因是存储数据的table下标是通过数组当前最大长度来计算的

2.4.1 HashMap的下标计算详解

我们来看一下HashMap的下标是如何通过hash计算的

/**
 * Implements Map.put and related methods. 实现 Map.put 和相关方法。
 *
 * @param hash         键的散列,就是上面hash()方法计算得出的hash值
 * @param key          你要存放的key
 * @param value        你要存放的value
 * @param onlyIfAbsent 如果为真,则不更改现有值
 * @param evict        如果为 false,则表处于创建模式。
 * @return             如果当前key已经存在,则会返回老的value,否则返回null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 当前table为空则去初始化table,并把table的长度赋值给n
    if ((tab = table) == null || (n = tab.length) == 0)
        // resize() 这个方法是对当前table进行初始化和扩容用的,后续会讲解
        n = (tab = resize()).length;
    /*
     * n = tab.length 当前HashMap的长度
     * i=(n - 1) & hash
     */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    ...

我们来看看 i 的计算方式:

i=(n-1) & hash

用当前table(存放数据的数组)的最大长度(table.length) - 1 然后跟传入的key位与计算,得到的值就是数组下标
例:

  • 定义n=16;key=6458254;n=16; key = 6458254;
  • 然后来计算keyhash
  • 调用hashCode()方法计算key的hash值是:6458348
  • 结果转换如下:
i=(161)位与6458348;i=(16-1) 位与 6458348;

位与:&

  • 将其转换为二进制进行位与计算
150000000000000000000000000000111115:00000000000000000000000000001111
6458348000000000110001010001011111011006458348:00000000011000101000101111101100
  • 开始位与计算15 & 6458348

  • 得到:i = 12

这就是最终结果,也是hashMap中计算下标公式,通过这个公式将所有key的下标都控制在table数组内
这一节内容已经分享完成,剩余内容在我的博客内可以找到

如果这篇文章对您理解HashMap有帮助,请帮忙分享一下~
小小码农在这给各位看官锤锤腿~