室友只用一把王者的时间,就学会了HashMap。

Posted 步尔斯特

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了室友只用一把王者的时间,就学会了HashMap。相关的知识,希望对你有一定的参考价值。



🍎 相关阅读



一、HashMap简介

HashMap顶部有一段很长的注释,大概的介绍了HashMap。

1.1 原文

/**
 * Hash table based implementation of the <tt>Map</tt> interface.  This
 * implementation provides all of the optional map operations, and permits
 * <tt>null</tt> values and the <tt>null</tt> key.  (The <tt>HashMap</tt>
 * class is roughly equivalent to <tt>Hashtable</tt>, except that it is
 * unsynchronized and permits nulls.)  This class makes no guarantees as to
 * the order of the map; in particular, it does not guarantee that the order
 * will remain constant over time.
 *
 * <p>This implementation provides constant-time performance for the basic
 * operations (<tt>get</tt> and <tt>put</tt>), assuming the hash function
 * disperses the elements properly among the buckets.  Iteration over
 * collection views requires time proportional to the "capacity" of the
 * <tt>HashMap</tt> instance (the number of buckets) plus its size (the number
 * of key-value mappings).  Thus, it's very important not to set the initial
 * capacity too high (or the load factor too low) if iteration performance is
 * important.
 *
 * <p>An instance of <tt>HashMap</tt> has two parameters that affect its
 * performance: <i>initial capacity</i> and <i>load factor</i>.  The
 * <i>capacity</i> is the number of buckets in the hash table, and the initial
 * capacity is simply the capacity at the time the hash table is created.  The
 * <i>load factor</i> is a measure of how full the hash table is allowed to
 * get before its capacity is automatically increased.  When the number of
 * entries in the hash table exceeds the product of the load factor and the
 * current capacity, the hash table is <i>rehashed</i> (that is, internal data
 * structures are rebuilt) so that the hash table has approximately twice the
 * number of buckets.
 *
 * <p>As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the <tt>HashMap</tt> class, including
 * <tt>get</tt> and <tt>put</tt>).  The expected number of entries in
 * the map and its load factor should be taken into account when
 * setting its initial capacity, so as to minimize the number of
 * rehash operations.  If the initial capacity is greater than the
 * maximum number of entries divided by the load factor, no rehash
 * operations will ever occur.
 *
 * <p>If many mappings are to be stored in a <tt>HashMap</tt>
 * instance, creating it with a sufficiently large capacity will allow
 * the mappings to be stored more efficiently than letting it perform
 * automatic rehashing as needed to grow the table.  Note that using
 * many keys with the same @code hashCode() is a sure way to slow
 * down performance of any hash table. To ameliorate impact, when keys
 * are @link Comparable, this class may use comparison order among
 * keys to help break ties.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a hash map concurrently, and at least one of
 * the threads modifies the map structurally, it <i>must</i> be
 * synchronized externally.  (A structural modification is any operation
 * that adds or deletes one or more mappings; merely changing the value
 * associated with a key that an instance already contains is not a
 * structural modification.)  This is typically accomplished by
 * synchronizing on some object that naturally encapsulates the map.
 *
 * If no such object exists, the map should be "wrapped" using the
 * @link Collections#synchronizedMap Collections.synchronizedMap
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the map:<pre>
 *   Map m = Collections.synchronizedMap(new HashMap(...));</pre>
 *
 * <p>The iterators returned by all of this class's "collection view methods"
 * are <i>fail-fast</i>: if the map is structurally modified at any time after
 * the iterator is created, in any way except through the iterator's own
 * <tt>remove</tt> method, the iterator will throw a
 * @link ConcurrentModificationException.  Thus, in the face of concurrent
 * modification, the iterator fails quickly and cleanly, rather than risking
 * arbitrary, non-deterministic behavior at an undetermined time in the
 * future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw <tt>ConcurrentModificationException</tt> on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness: <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * <p>This class is a member of the
 * <a href="@docRoot/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 *
 * @author  Doug Lea
 * @author  Josh Bloch
 * @author  Arthur van Hoff
 * @author  Neal Gafter
 * @see     Object#hashCode()
 * @see     Collection
 * @see     Map
 * @see     TreeMap
 * @see     Hashtable
 * @since   1.2
 */
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
...


1.2 翻译

Map接口是基于哈希表实现的,这个实现为map提供了许多操作方法,允许key是null,或者values是null。HashMap大致和Hashtable一样,不同之处在于HashMap线程不同步,同时允许有null值。这个类不保证map的顺序,特别是它不能保证每次都顺序不变。

HashMap的实现为基础操作get和put方法提供了不变的性能表现,假设哈希函数在buckets中按比例分散元素,迭代器需要时间比例对hashmap实例(buckets的数量)的容量进行扩容(key-value映射的数量)。因此,如果迭代器性能很重要时,就不要给它的初始容量设置太高(或者加载因子太低)

一个Hashmap的实例会有两个参数影响它的性能表现:initial capacity初始值和load factor加载因子,capacity是哈希表中buckets的数量,初始值就是哈希表在被创建时的容量。加载因子是用来判断当哈希表到多满时才被允许自动扩容。当哈希表中的键值对超过了负载因子的乘积和当前的容量时,哈希表会通过rehashed的方法重新计算,(重建内部数据结构)这样一来哈希表大致会变成原有buckets的数量的两倍。

作为一条普遍的规则,默认的加载因子是0.75,它提供了很好的性能表现在时间和空间成本上。比0.75高就会降低整体空间,但是增加了查找的成本(这反应在绝大多数的HashMap操作,包括get和put方法)。当设置init capacity初始化值时,键值对的数量和它的负载因子应该被考虑进去,这样才能最小程度的降低重新计算哈希值的操作。如果初始化值比在被负载因子分发后的最大键值对数还要大时,重新计算哈希值的操作就会发生。

如果许多映射关系被存到一个hashMap实例中,创建足够大的容量让键值对被存储,会比让他们自动计算哈希值去扩容更高效。要注意的是,拥有同样哈希值的keys会降低所有哈希表的性能。为了改进这个影响,当keys在调用Comparable接口时,这个类利用比较顺序来帮助keys打破僵局。

需要注意的是这个方法不是同步的。如果多线程同时进入一个哈希映射时,并且至少有一个线程改变了map的结构时,它必须要在外面被同步。(结构性的修改可以时任何添加,删除更多键值对的操作,几乎不改变一个键的值,就说明这不是一个结构性的改变)。这些通常发生在当一些对象要被同步放进map时。

如果没有这样的对象存在,map必须要使用Collections.synchronizedMap方法包围,这是在创建时最好的做法,阻止意外的不同步map操作。
Map m = Collections.synchronizedMap(new HashMap(…));

迭代器返回了几乎所有集合都有的fail-fast的方法:如果map在迭代器被创建后的任何时间被结构性的改变,那么除了迭代器使用自己的remove方法,其他情况下迭代器都被抛出一个一场ConcurrentModificationException。因此,在同时被改变的时候,迭代器会比随时可能发生的风险,或者在不确定的时间下发生的不确定行为,失败的更快更利落。

需要注意的是,一个迭代器的快速失败行为不能被保证,一般来说,不可能完全保证不同步情况下的的同时修改结构的事情发生。快速失败迭代器会在一个最好的基础上抛出ConcurrentModificationException异常。因此,如果一个程序通过这个异常来判断它对错的行为是错误的,迭代器的快速失败行为只能用于检测bugs

1.3 一语中的

HashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null。遍历时无序。

其底层数据结构是数组称之为哈希桶,每个桶里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。

在JDK8中,当链表长度达到8,会转化成红黑树,此过程称为树化,以提升它的查询、插入效率,它实现了Map<K,V>, Cloneable, Serializable接口。当链表长度小于6,会从红黑树转化为链表。

因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。
当HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方。
这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。

而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。

因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。

但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。

扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)

扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。

扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。

因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量

如果追加节点后,链表数量>=8,则转化为红黑树

由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。属于无序集合。

HashMap的源码中,充斥个各种位运算代替常规运算的地方,以提升效率:

  • 与运算替代模运算。用(table.length-1) & hash替代(table.length) % hash
  • 用if ((e.hash & oldCap) == 0)判断扩容后,节点e处于低区还是高区。

为了大家更好的理解HashMap源码,建议大家先深入了解一下 ArrayList 和 LinkedList :

如果小伙伴们不了解"fail-fast"机制,可以参考如下文章:
深入浅出fail-fast机制

1.4 线程安全性

HashMap的线程不安全,HashMap中的方法都是不同步的

put方法不同
addEntry方法依然不是同步的,所以导致了线程不安全出现伤处问题,其他类似操作不再说明。

resize方法不同步
扩容过程中,会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。

当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

如何线程安全的使用 HashMap:

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map

为了减少这类基础问题的发生,建议使用ConcurrentHashMap替换HashMap。

<1> ConcurrentHashMap1.8之前使用segment分段锁,jdk1.8以后通过CAS算法实现无锁化,目标都是为了实现轻量级线程同步。相比HashTable性能高很多。

<2> ConcurrentHashMap没有fastfail问题,可以减少程序错误的发生。

CHM 性能是明显优于 Hashtable 和 SynchronizedMap 的,CHM 花费的时间比前两个的一半还少。

1.5 优劣分析

HashMap 树化
链表的查找性能是O(n),若节点数较小性能不回收太大影响,但数据较大时差距将逐渐显现。树的查找性能是O(log(n)),性能优势瞬间体现

  • 优点:超级快速的查询速度,如果有人问你什么数据结构可以达到O(1)的时间复杂度,没错是HashMap,动态的可变长存储数据(和数组相比较而言)

  • 缺点:需要额外计算一次hash值,如果处理不当会占用额外的空间

二、定义

我们先来看看HashMap的定义:

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

HashMap的类结构图

如何查看类的完整结构图可以参考如下文章:
IDEA如何查看类的完整结构图

三、数据结构

四、域的解读

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 
    //序列号,序列化的时候使用。
    private static final long serialVersionUID = 362498820763181265L;
    /**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。**/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量,2的30次方。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //加载因子,用于扩容使用。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当某个桶节点数量大于8时,会转换为红黑树。
    static final int TREEIFY_THRESHOLD = 8;
    //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
    static final int UNTREEIFY_THRESHOLD = 6;
    //当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组,transient关键字表示该属性不能被序列化
    transient Node<K,V>[] table;
    //将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
    transient Set<Map.Entry<K,V>> entrySet;
    //元素数量
    transient int size;
    //统计该map修改的次数
    transient int modCount;
    //临界值,也就是元素数量达到临界值时,会进行扩容。
    int threshold;
    //也是加载因子,只不过这个是变量。
    final float loadFactor;  

其中最主要的成员变量

table变量:HashMap的底层数据结构,是Node类的实体数组,Node是一个静态内部类,一种数组和链表相结合的复合结构,用于保存key-value对;

size变量:表示已存储的HashMap的key-value对的数量;

loadFactor变量:装载因子,用于衡量满的程度,默认值为0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;);

threshold变量:临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子;

capacity:并不是一个成员变量,但却是一个必须要知道的概念,表示容量,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;)

为什么默认容量大小为16,加载因子为0.75,主要原因是这两个常量的值都是经过大量的计算和统计得出来的最优解,仅仅是这样而已。

链表节点Node

    static class Node<K,V> implements Map.Entry<K,V> 
        final int hash;//哈希值
        final K key;//key
        V value;//value
        Node<K,V> next;//链表后置节点
 
        Node(int hash, K key, V value, Node<K,V> next) 
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        
 
        public final K getKey()         return key; 
        public final V getValue()       return value; 
        public final String toString()  return key + "=" + value; 
 
        //每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。
        public final int hashCode() 
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        
        //设置新的value 同时返回旧value
        public final V setValue(V newValue) 
            V oldValue = value;
            value = newValue;
            return oldValue;
        
 
        public final boolean equals(Object o) 
            if (o == this)
                return true;
            if (o instanceof Map.Entry) 
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            
            return false;
        
    //这是一个单链表,每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。

五、构造方法

HashMap 共提供了 4 种 构造方法,满足各种常见场景下对容量的需求

	// 第1种:创建一个 HashMap 并指定 容量(initialCapacity) 和装载因子(loadFactor)
    public HashMap(int initialCapacity, float loadFactor) 
    	// 指定容量不可小于0,但可设置为 0 。之后通过put()添加元素时,会resize()
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 如果指定的容量超过了最大值,则自动置为最大值,也就是 1 << 30(也就是2的30次方)
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 装载因子不可小于等于 0 或 非数字(NaN)
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 初始化装载因子
        this.loadFactor = loadFactor;
        // 初始化下次需要调整到的容量(容量*装载因子)。
        this.threshold = tableSizeFor(initialCapacity);
    

	// 第2种:创建一个指定容量的 HashMap,装载因子使用默认的 0.75
    public HashMap(int initialCapacity) 
    	// 调用上个构造方法初始化
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    

	// 第3种:创建一个默认初始值的 HashMap ,容量为16,装载因子为0.75
    public HashMap() 
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    

	// 第4种:创建一个 Hashmap 并将 m 内包含的所有元素存入
    public HashMap(Map<? extends K, ? extends V> m) 
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    

tableSizeFor(int cap)
获取一个既大于 cap 又最接近 cap 的 2 的整数次幂数值


 	// 假设 cap = 128
    static final int tableSizeFor(int cap) 
        int n = cap - 1; // 则 n = 127 = 01111111
        n |= n >>> 1; // n = 01111111 , n >>> 1 = 00111111 , 按位或后  n = 01111111
        n |= n >>> 2; // n = 01111111 , n >>> 1 = 00011111, 按位或后  n = 01111111
        n |= n >>> 4; // n = 01111111 , n >>> 1 = 00000111, 按位或后  n = 01111111
        n |= n >>> 8; // n = 01111111 , n >>> 1 = 00000000, 按位或后  n = 01111111
        n |= n >>> 16; // n = 01111111 , n >>> 1 = 00000000, 按位或后  n = 01111111
        // 如果 n 小于 0 则返回 1,否则判断 n 是否大于等于最大容量,是的话返回最大容量,不是就返回 n+1(也就是128)
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    

六、核心方法

6.1 tableSizeFor(int cap)

获取一个既大于 cap 又最接近 cap 的 2 的整数次幂数值


 	// 假设 cap = 128
    static final int tableSizeFor(int cap) 
        int n = cap - 1; // 则 n = 127 = 01111111
        n |= n >>> 1; // n = 01111111 , n >>> 1 = 00111111 , 按位或后  n = 01111111
        n |= n >>> 2; // n = 01111111 , n >>> 1 = 00011111, 按位或后  n = 01111111
        n |= n >>> 4; // n = 01111111 , n >>> 1 = 00000111, 按位或后  n = 01111111
        n |= n >>> 8; // n = 01111111 , n >>> 1 = 00000000, 按位或后  n = 01111111
        n |= n >>> 16; // n = 01111111 , n >>> 1 = 00000000, 按位或后  n = 01111111
        // 如果 n 小于 0 则返回 1,否则判断 n 是否大于等于最大容量,是的话返回最大容量,不是就返回 n+1(也就是128)
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    

图解

6.2 hash() 方法

static final int hash(Object key) 
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);


这个根据key取hash值的函数,称之为“扰动函数”

最开始的hashCode: 1111 1111 1111 1111 0100 1100 0000 1010
右移16位的hashCode:0000 0000 0000 0000 1111 1111 1111 1111
异或运算后的hash值: 1111 1111 1111 1111 1011 0011 1111 0101

将hashcode 与 hashcode的低16位做异或运算,混合了高位和低位得出的最终hash值(扰动算法),冲突的概率就小多了。

而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。

因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。

但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。

扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)

6.3 put(K key, V value)

    public V put(K key, V value) 
        /**四个参数,第一个hash值,第四个参数表示如果该key存在值,如果为null的话,则插入新的value,最后一个参数,在hashMap中没有用,可以不用管,使用默认的即可**/
        return putVal(hash(key), key, value, false, true);
    
 
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) 
        //tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = 「数据结构」室友打一把王者就学会了冒泡排序算法

51单片机室友用一把王者时间,学会了去使用数码管。

翠花一把王者的时间,我就学会了Nginx

他居然只用了一把王者的时间就入门了·大数据?

一把王者的时间,我就学会了Nginx

我室友打了一把王者6分钟我搞成了Oracle12c的安装以及如何创建数据库