• HashMap


    HashMap

    初识HashMap

    之前的List,讲了ArrayList、LinkedList,最后讲到了CopyOnWriteArrayList,就前两者而言,反映的是两种思想:

    (1)ArrayList以数组形式实现,顺序插入、查找快,插入、删除较慢

    (2)LinkedList以链表形式实现,顺序插入、查找较慢,插入、删除方便

    那么是否有一种数据结构能够结合上面两种的优点呢?有,答案就是HashMap。

    HashMap是一种非常常见、方便和有用的集合,是一种键值对(K-V)形式的存储结构,下面将还是用图示的方式解读HashMap的实现原理,

     

    四个关注点在HashMap上的答案

    关 注 点结 论
    HashMap是否允许空 Key和Value都允许为空
    HashMap是否允许重复数据 Key重复会覆盖、Value允许重复
    HashMap是否有序 无序,特别说明这个无序指的是遍历HashMap的时候,得到的元素的顺序基本不可能是put的顺序
    HashMap是否线程安全 非线程安全

    先看下hashMap的基本组成:

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

    图示:

     

     

    从HashMap的Entry看得出,Entry组成的是一个单向链表,因为里面只有Entry的后继Entry,而没有Entry的前驱Entry,在new一个HashMap的时候,默认创建一个大小为16的数组(DEFAULT_INITIAL_CAPACITY为16),如图:

     

    添加数据

    先来一段代码:

    1 public static void main(String[] args)
    2 {
    3     Map<String, String> map = new HashMap<String, String>();
    4     map.put("111", "111");
    5     map.put("222", "222");
    6 }

    看下底层发生了什么:

    1 public V put(K key, V value) {
    2     if (key == null)
    3         return putForNullKey(value);
    4     int hash = hash(key.hashCode());
    5     int i = indexFor(hash, table.length);
    6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    7        Object k;
    8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    9             V oldValue = e.value;
    10             e.value = value;
    11             e.recordAccess(this);
    12             return oldValue;
    13         }
    14     }
    15
    16     modCount++;
    17     addEntry(hash, key, value, i);
    18     return null;
    19 }

    1 static int hash(int h) {
    2     // This function ensures that hashCodes that differ only by
    3     // constant multiples at each bit position have a bounded
    4     // number of collisions (approximately 8 at default load factor).
    5     h ^= (h >>> 20) ^ (h >>> 12);
    6     return h ^ (h >>> 7) ^ (h >>> 4);
    7 }

    1 static int indexFor(int h, int length) {
    2     return h & (length-1);
    3 }

    1、第2行~第3行就是HashMap允许Key值为空的原因,空的Key会默认放在第0位的数组位置上

    2、第4行拿到Key值的HashCode,由于HashCode是Object的方法,因此每个对象都有一个HashCode,对这个HashCode做一次hash计算。按照JDK源码注释的说法,这次hash的作用是根据给定的HashCode对它做一次打乱的操作,防止一些糟糕的Hash算法产生的糟糕的Hash值,至于为什么要防止糟糕的Hash值,HashMap添加元素的最后会讲到

    3、第5行根据重新计算的HashCode,对Entry数组的大小取模得到一个Entry数组的位置。看到这里使用了&,移位加快一点代码运行效率。另外,这个取模操作的正确性依赖于length必须是2的N次幂,这个熟悉二进制的朋友一定理解,因此注意HashMap构造函数中,如果你指定HashMap初始数组的大小initialCapacity,如果initialCapacity不是2的N次幂,HashMap会算出大于initialCapacity的最小2的N次幂的值,作为Entry数组的初始化大小。这里为了讲解方便,我们假定字符串111和字符串222算出来的i都是1

    4、第6行~第14行会先判断一下原数据结构中是否存在相同的Key值,存在则覆盖并返回,不执行后面的代码。注意一下recordAccess这个方法,它也是HashMap的子类比如LinkedHashMap用的,HashMap中这个方法为空。另外,注意一点,对比Key是否相同,是先比HashCode是否相同,HashCode相同再判断equals是否为true,这样大大增加了HashMap的效率,对HashCode不熟悉的朋友可以看一下我的这篇文章讲讲HashCode的作用

    5、第16行的modeCount++是用于fail-fast机制的,每次修改HashMap数据结构的时候都会自增一次这个值

    图示:

     

    扩容

    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
       if (size++ >= threshold)
           resize(2 * table.length);
    }
    void resize(int newCapacity) {
       Entry[] oldTable = table;
       int oldCapacity = oldTable.length;
       if (oldCapacity == MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return;
      }

       Entry[] newTable = new Entry[newCapacity];
       transfer(newTable);
       table = newTable;
       threshold = (int)(newCapacity * loadFactor);
    }
    void transfer(Entry[] newTable) {
       Entry[] src = table;
       int newCapacity = newTable.length;
       for (int j = 0; j < src.length; j++) {
           Entry<K,V> e = src[j];
           if (e != null) {
               src[j] = null;
               do {
                   Entry<K,V> next = e.next;
                   int i = indexFor(e.hash, newCapacity);
                   e.next = newTable[i];
                   newTable[i] = e;
                   e = next;
              } while (e != null);
          }
      }
    }

     

    我总结一下这三段代码,HashMap一次扩容的过程应该是:

    1、取当前table的2倍作为新table的大小

    2、根据算出的新table的大小new出一个新的Entry数组来,名为newTable

    3、轮询原table的每一个位置,将每个位置上连接的Entry,算出在新table上的位置,并以链表形式连接

    4、原table上的所有Entry全部轮询完毕之后,意味着原table上面的所有Entry已经移到了新的table上,HashMap中的table指向newTable

    这样就完成了一次扩容,用图表示是这样的:

     

    删除

    1 public V remove(Object key) {
    2     Entry<K,V> e = removeEntryForKey(key);
    3     return (e == null ? null : e.value);
    4 }

    1 final Entry<K,V> removeEntryForKey(Object key) {
    2     int hash = (key == null) ? 0 : hash(key.hashCode());
    3     int i = indexFor(hash, table.length);
    4     Entry<K,V> prev = table[i];
    5     Entry<K,V> e = prev;
    6
    7     while (e != null) {
    8         Entry<K,V> next = e.next;
    9         Object k;
    10         if (e.hash == hash &&
    11             ((k = e.key) == key || (key != null && key.equals(k)))) {
    12             modCount++;
    13             size--;
    14             if (prev == e)
    15                 table[i] = next;
    16             else
    17                 prev.next = next;
    18             e.recordRemoval(this);
    19             return e;
    20         }
    21         prev = e;
    22         e = next;
    23     }
    24
    25     return e;
    26 }

    分析一下remove元素的时候做了几步:

    1、根据key的hash找到待删除的键值对位于table的哪个位置上

    2、记录一个prev表示待删除的Entry的前一个位置Entry,e可以认为是当前位置

    3、从table[i]开始遍历链表,假如找到了匹配的Entry,要做一个判断,这个Entry是不是table[i]:

    (1)是的话,也就是第14行~第15行,table[i]就直接是table[i]的下一个节点,后面的都不需要动

    (2)不是的话,也就是第16行~第17行,e的前一个Entry也就是prev,prev的next指向e的后一个节点,也就是next,这样,e所代表的Entry就被踢出了,e的前后Entry就连起来了

    remove("111")用图表示就是:

    整个过程只需要修改一个节点的next的值即可,非常方便。

    修改数据

    1 public V put(K key, V value) {
    2     if (key == null)
    3         return putForNullKey(value);
    4     int hash = hash(key.hashCode());
    5     int i = indexFor(hash, table.length);
       //进行循环
    6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    7         Object k;
        //判断key是否相同,相同则进行替换
    8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    9             V oldValue = e.value;
    10             e.value = value;
    11             e.recordAccess(this);
    12             return oldValue;
    13         }
    14     }
    15     modCount++;
    16     addEntry(hash, key, value, i);
    17     return null;
    18 }

     

    再谈HashCode的重要性

    前面讲到了,HashMap中对Key的HashCode要做一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode,那么为什么要防止糟糕的HashCode?

    糟糕的HashCode意味着的是Hash冲突,即多个不同的Key可能得到的是同一个HashCode,糟糕的Hash算法意味着的就是Hash冲突的概率增大,这意味着HashMap的性能将下降,表现在两方面:

    1、有10个Key,可能6个Key的HashCode都相同,另外四个Key所在的Entry均匀分布在table的位置上,而某一个位置上却连接了6个Entry。这就失去了HashMap的意义,HashMap这种数据结构性高性能的前提是,Entry均匀地分布在table位置上,但现在确是1 1 1 1 6的分布。所以,我们要求HashCode有很强的随机性,这样就尽可能地可以保证了Entry分布的随机性,提升了HashMap的效率。

    2、HashMap在一个某个table位置上遍历链表的时候的代码:

    if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

    看到,由于采用了"&&"运算符,因此先比较HashCode,HashCode都不相同就直接pass了,不会再进行equals比较了。HashCode因为是int值,比较速度非常快,而equals方法往往会对比一系列的内容,速度会慢一些。Hash冲突的概率大,意味着equals比较的次数势必增多,必然降低了HashMap的效率了。

    简单来说,就是要使hashCode要不重复或减少重复,才能使Entry均匀的分布在table上,所以需要好的hash算法

     

    HashMap的table为什么是transient的

    一个非常细节的地方:

    transient Entry[] table;

    看到table用了transient修饰,也就是说table里面的内容全都不会被序列化,不知道大家有没有想过这么写的原因?

    在我看来,这么写是非常必要的。因为HashMap是基于HashCode的,HashCode作为Object的方法,是native的:

    public native int hashCode();

    这意味着的是:HashCode和底层实现相关,不同的虚拟机可能有不同的HashCode算法。再进一步说得明白些就是,可能同一个Key在虚拟机A上的HashCode=1,在虚拟机B上的HashCode=2,在虚拟机C上的HashCode=3。

    这就有问题了,Java自诞生以来,就以跨平台性作为最大卖点,好了,如果table不被transient修饰,在虚拟机A上可以用的程序到虚拟机B上可以用的程序就不能用了,失去了跨平台性,因为:

    1、Key在虚拟机A上的HashCode=100,连在table[4]上

    2、Key在虚拟机B上的HashCode=101,这样,就去table[5]上找Key,明显找不到

     

    HashMap和Hashtable的区别

    HashMap和Hashtable是一组相似的键值对集合,它们的区别也是面试常被问的问题之一,我这里简单总结一下HashMap和Hashtable的区别:

    1、Hashtable是线程安全的,Hashtable所有对外提供的方法都使用了synchronized,也就是同步,而HashMap则是线程非安全的

    2、Hashtable不允许空的value,空的value将导致空指针异常,而HashMap则无所谓,没有这方面的限制

    3、上面两个缺点是最主要的区别,另外一个区别无关紧要,我只是提一下,就是两个的rehash算法不同

  • 相关阅读:
    test
    c# cook book -Linq 关于Object的比较
    关于UnitOfWork
    autofac学习
    webapi 开启跨域支持
    httpclient通过post提交到webapi
    jQuery之元素查找
    jQuery之过滤元素
    jQuery之回到顶部
    jQuery之_元素滚动
  • 原文地址:https://www.cnblogs.com/create-and-orange/p/11236863.html
Copyright © 2020-2023  润新知