zoukankan      html  css  js  c++  java
  • HashMap

    https://juejin.im/post/5dee6f54f265da33ba5a79c8

    HashMap实现了Map接口,并继承 AbstractMap 抽象类,其中 Map 接口定义了键值映射规则。AbstractMap 抽象类提供了 Map 接口的骨干实现,以最大限度地减少实现Map接口所需的工作。

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable{
    ...
    }

    初始容量负载因子(一般默认0.75),这两个参数是影响HashMap性能的重要参数。其中,容量表示哈希表中桶的数量 (table 数组的大小),初始容量是创建哈希表时桶的数量;负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

    哈希的相关概念

      Hash 就是把任意长度的输入(又叫做预映射, pre-image),通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。这种转换是一种 压缩映射 ,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。简单的说,就是一种将任意长度的消息压缩到某一固定长度的息摘要函数。

     1  /**
     2      * Constructs an empty HashMap with the default initial capacity
     3      * (16) and the default load factor (0.75).
     4      */
     5     public HashMap() {
     6 
     7         //负载因子:用于衡量的是一个散列表的空间的使用程度
     8         this.loadFactor = DEFAULT_LOAD_FACTOR; 
     9 
    10         //HashMap进行扩容的阈值,它的值等于 HashMap 的容量乘以负载因子
    11         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    12 
    13         // HashMap的底层实现仍是数组,只是数组的每一项都是一条链
    14         table = new Entry[DEFAULT_INITIAL_CAPACITY];
    15 
    16         init();
    17     }
    18 
    19 
    20 
    21 static class Entry<K,V> implements Map.Entry<K,V> {
    22 
    23     final K key;     // 键值对的键
    24     V value;        // 键值对的值
    25     Entry<K,V> next;    // 下一个节点
    26     final int hash;     // hash(key.hashCode())方法的返回值
    27 
    28     /**
    29      * Creates new entry.
    30      */
    31     Entry(int h, K k, V v, Entry<K,V> n) {     // Entry 的构造函数
    32         value = v;
    33         next = n;
    34         key = k;
    35         hash = h;
    36     }
    37 
    38     ......
    39 
    40 }
    View Code

    其中,Entry为HashMap的内部类,实现了 Map.Entry 接口,其包含了键key、值value、下一个节点next,以及hash值四个属性。事实上,Entry 是构成哈希表的基石,是哈希表所存储的元素的具体形式。

    HashMap 的存储实现

    在 HashMap 中,键值对的存储是通过 put(key,vlaue) 方法来实现的,其源码如下:

     1 public V put(K key, V value) {
     2 
     3         //当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置 
     4         if (key == null)
     5             return putForNullKey(value); 
     6 
     7         //根据key的hashCode计算hash值
     8         int hash = hash(key.hashCode());             //  ------- (1)
     9 
    10         //计算该键值对在数组中的存储位置(哪个桶)
    11         int i = indexFor(hash, table.length);              // ------- (2)
    12 
    13         //在table的第i个桶上进行迭代,寻找 key 保存的位置
    14         for (Entry<K,V> e = table[i]; e != null; e = e.next) {      // ------- (3)
    15             Object k;
    16             //判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
    17             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    18                 V oldValue = e.value;
    19                 e.value = value;
    20                 e.recordAccess(this);
    21                 return oldValue;    // 返回旧值
    22             }
    23         }
    24 
    25         modCount++; //修改次数增加1,快速失败机制
    26 
    27         //原HashMap中无该映射,将该添加至该链的链头
    28         addEntry(hash, key, value, i);            
    29         return null;
    30     }
    View Code

    在上述的 put(key,vlaue) 方法的源码中,我们标出了 HashMap 中的哈希策略(即(1)、(2)两处),hash() 方法用于对Key的hashCode进行重新计算,而 indexFor() 方法用于生成这个Entry对象的插入位置。当计算出来的hash值与hashMap的(length-1)做了&运算后,会得到位于区间[0,length-1]的一个值。特别地,这个值分布的越均匀, HashMap 的空间利用率也就越高,存取效率也就越好。

    使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀。更多关于该 hash(int h)方法的介绍请见《HashMap hash方法分析》

    我们知道,HashMap的底层数组长度总是2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模要快得多,这是HashMap在速度上的一个优化。

    内部源码如下:

      1 /**
      2      * Offloaded version of put for null keys
      3      */
      4     private V putForNullKey(V value) {
      5         // 若key==null,则将其放入table的第一个桶,即 table[0]
      6         for (Entry<K,V> e = table[0]; e != null; e = e.next) {   
      7             if (e.key == null) {   // 若已经存在key为null的键,则替换其值,并返回旧值
      8                 V oldValue = e.value;
      9                 e.value = value;
     10                 e.recordAccess(this);
     11                 return oldValue;
     12             }
     13         }
     14         modCount++;        // 快速失败
     15         addEntry(0, null, value, 0);       // 否则,将其添加到 table[0] 的桶中
     16         return null;
     17     }
     18 ————————————————
     19 /**
     20      * Applies a supplemental hash function to a given hashCode, which
     21      * defends against poor quality hash functions.  This is critical
     22      * because HashMap uses power-of-two length hash tables, that
     23      * otherwise encounter collisions for hashCodes that do not differ
     24      * in lower bits. 
     25      * 
     26      * Note: Null keys always map to hash 0, thus index 0.
     27      */
     28     static int hash(int h) {
     29         // This function ensures that hashCodes that differ only by
     30         // constant multiples at each bit position have a bounded
     31         // number of collisions (approximately 8 at default load factor).
     32         h ^= (h >>> 20) ^ (h >>> 12);
     33         return h ^ (h >>> 7) ^ (h >>> 4);
     34     }
     35 ————————————————
     36 /**
     37      * Returns index for hash code h.
     38      */
     39     static int indexFor(int h, int length) {
     40         return h & (length-1);  // 作用等价于取模运算,但这种方式效率更高
     41     }
     42 ————————————————
     43 /**
     44      * Adds a new entry with the specified key, value and hash code to
     45      * the specified bucket.  It is the responsibility of this
     46      * method to resize the table if appropriate.
     47      *
     48      * Subclass overrides this to alter the behavior of put method.
     49      * 
     50      * 永远都是在链表的表头添加新元素
     51      */
     52     void addEntry(int hash, K key, V value, int bucketIndex) {
     53 
     54         //获取bucketIndex处的链表
     55         Entry<K,V> e = table[bucketIndex];
     56 
     57         //将新创建的 Entry 链入 bucketIndex处的链表的表头 
     58         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
     59 
     60         //若HashMap中元素的个数超过极限值 threshold,则容量扩大两倍
     61         if (size++ >= threshold)
     62             resize(2 * table.length);
     63     }
     64 ————————————————
     65 /**
     66      * Rehashes the contents of this map into a new array with a
     67      * larger capacity.  This method is called automatically when the
     68      * number of keys in this map reaches its threshold.
     69      *
     70      * If current capacity is MAXIMUM_CAPACITY, this method does not
     71      * resize the map, but sets threshold to Integer.MAX_VALUE.
     72      * This has the effect of preventing future calls.
     73      *
     74      * @param newCapacity the new capacity, MUST be a power of two;
     75      *        must be greater than current capacity unless current
     76      *        capacity is MAXIMUM_CAPACITY (in which case value
     77      *        is irrelevant).
    随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,这样势必会影响HashMap的存取速度。为了保证
    HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。
    但是,不得不说,扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新table数组中的位置并进行复制处理。所以,如果我们能够提前预知HashMap
    中元素的个数,那么在构造HashMap时预设元素的个数能够有效的提高HashMap的性能。
     78      */
     79     void resize(int newCapacity) {
     80         Entry[] oldTable = table;
     81         int oldCapacity = oldTable.length;
     82 
     83         // 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUE
     84         if (oldCapacity == MAXIMUM_CAPACITY) {  
     85             threshold = Integer.MAX_VALUE;
     86             return;             // 直接返回
     87         }
     88 
     89         // 否则,创建一个更大的数组
     90         Entry[] newTable = new Entry[newCapacity];
     91 
     92         //将每条Entry重新哈希到新的数组中
     93         transfer(newTable);
     94 
     95         table = newTable;
     96         threshold = (int)(newCapacity * loadFactor);  // 重新设定 threshold
     97     }
     98 ————————————————
     99  /**
    100      * Transfers all entries from current table to newTable.重哈希的主要是一个重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程
    101      */
    102     void transfer(Entry[] newTable) {
    103 
    104         // 将原数组 table 赋给数组 src
    105         Entry[] src = table;
    106         int newCapacity = newTable.length;
    107 
    108         // 将数组 src 中的每条链重新添加到 newTable 中
    109         for (int j = 0; j < src.length; j++) {
    110             Entry<K,V> e = src[j];
    111             if (e != null) {
    112                 src[j] = null;   // src 回收
    113 
    114                 // 将每条链的每个元素依次添加到 newTable 中相应的桶中
    115                 do {
    116                     Entry<K,V> next = e.next;
    117 
    118                     // e.hash指的是 hash(key.hashCode())的返回值;
    119                     // 计算在newTable中的位置,注意原来在同一条子链上的元素可能被分配到不同的子链
    120                     int i = indexFor(e.hash, newCapacity);   
    121                     e.next = newTable[i];
    122                     newTable[i] = e;
    123                     e = next;
    124                 } while (e != null);
    125             }
    126         }
    127     }
    128 ————————————————

    总而言之,上述的hash()方法和indexFor()方法的作用只有一个:保证元素均匀分布到table的每个桶中以便充分利用空间。

    HashMap 永远都是在链表的表头添加新元素。此外,若HashMap中元素的个数超过极限值 threshold,其将进行扩容操作,一般情况下,容量将扩大至原来的两倍。

    HashMap 的读取实现

     1 /**
     2      * Returns the value to which the specified key is mapped,
     3      * or {@code null} if this map contains no mapping for the key.
     4      *
     5      * <p>More formally, if this map contains a mapping from a key
     6      * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     7      * key.equals(k))}, then this method returns {@code v}; otherwise
     8      * it returns {@code null}.  (There can be at most one such mapping.)
     9      *
    10      * <p>A return value of {@code null} does not <i>necessarily</i>
    11      * indicate that the map contains no mapping for the key; it's also
    12      * possible that the map explicitly maps the key to {@code null}.
    13      * The {@link #containsKey containsKey} operation may be used to
    14      * distinguish these two cases.
    15      *
    16      * @see #put(Object, Object)
    17      */
    18     public V get(Object key) {
    19         // 若为null,调用getForNullKey方法返回相对应的value
    20         if (key == null)
    21             // 从table的第一个桶中寻找 key 为 null 的映射;若不存在,直接返回null
    22             return getForNullKey();  
    23 
    24         // 根据该 key 的 hashCode 值计算它的 hash 码 
    25         int hash = hash(key.hashCode());
    26         // 找出 table 数组中对应的桶
    27         for (Entry<K,V> e = table[indexFor(hash, table.length)];
    28              e != null;
    29              e = e.next) {
    30             Object k;
    31             //若搜索的key与查找的key相同,则返回相对应的value
    32             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    33                 return e.value;
    34         }
    35         return null;
    36     }
    View Code
    /**
         * Offloaded version of get() to look up null keys.  Null keys map
         * to index 0.  This null case is split out into separate methods
         * for the sake of performance in the two most commonly used
         * operations (get and put), but incorporated with conditionals in
         * others.
         */
        private V getForNullKey() {
            // 键为NULL的键值对若存在,则必定在第一个桶中
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {
                if (e.key == null)
                    return e.value;
            }
            // 键为NULL的键值对若不存在,则直接返回 null
            return null;
        }

    HashMap 的底层数组长度为何总是2的n次方?

    HashMap 中的数据结构是一个数组链表,我们希望的是元素存放的越均匀越好。最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较Key,而且空间利用率最大。

    那如何计算才会分布最均匀呢?HashMap采用了一个分两步走的哈希策略:

    1.使用 hash() 方法用于对Key的hashCode进行重新计算,以防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使Key的hash值的分布尽量均匀;

    // HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n 
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;  

    2.使用 indexFor() 方法进行取余运算,以使Entry对象的插入位置尽量分布均匀

    《java提高篇(二三)—–HashMap》 

     总结:

    不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快;

    h&(length - 1) 就相当于对length取模,而且在速度、效率上比直接取模要快得多,即二者是等价不等效的,这是HashMap在速度和效率上的一个优化。

    https://blog.csdn.net/justloveyou_/article/details/62893086

    注:HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式

    https://blog.csdn.net/weixin_44460333/article/details/86770169

  • 相关阅读:
    使用C# 实现串口拨号器的SIM卡通信
    物联网协议Coap协议介绍
    C#实现简单的串口通信
    C#硬件访问(摄像头、麦克风)
    请问在电脑里PNP是什么意思啊?
    原码,反码,补码,及Java中数字表示方法
    3_PHP表达式_5_数据类型转换_类型强制转换
    3_PHP表达式_4_PHP运算符
    3_PHP表达式_5_数据类型转换_类型自动转换
    3_PHP表达式_3_有关变量或常量状态的函数
  • 原文地址:https://www.cnblogs.com/lingcheng7777/p/11672082.html
Copyright © 2011-2022 走看看