• java.util.HashMap 解析


         HashMap 是我们经常使用的一种数据结构。工作中会经常用到,面试也会总提到这个数据结构,找工作的时候,”HashTable 和HashMap的区别“被问到过没有?

         本文会从原理,JDK源码,项目使用多个角度来分析HashMap。

     

         1.HashMap是什么

           JDK文档中如是说”基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)不保证映射的顺序“

           里面大致包含如下意思:

           HashMap是Map的实现,因此它内部的元素都是K-V(键,值)组成的。

           HashMap内部元素是无序的

     

         2.jdk中如何实现一个HashMap

            HashMap在java.util包下,我们平时使用的类,有大部分都是这个包或者其子包的类

            JDK中实现类的定义

     

    Java代码   收藏代码
    1. public class HashMap<K,V>  
    2.     extends AbstractMap<K,V>  
    3.     implements Map<K,V>, Cloneable, Serializable  

           它实现了Map接口

           通常我们这么使用HashMap

     

    Java代码   收藏代码
    1. Map<Integer,String> maps=new HashMap<Integer,String>();  
    2. maps.put(1"a");  
    3. maps.put(2"b");  

     上面代码新建了一个HashMap并且往里插入了两个数据,这里不接受基本数据类型来做K,V

    如果你这么写的话,就会出问题了

    Java代码   收藏代码
    1. Map<int,double> maps=new HashMap<int,double>();  

     

       上面例子很简单可是你知道内部他们怎么实现的吗?

       我们来看看HashMap的构造方法

     

    Java代码   收藏代码
    1. public HashMap() {  
    2.        this.loadFactor = DEFAULT_LOAD_FACTOR;  
    3.        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
    4.        table = new Entry[DEFAULT_INITIAL_CAPACITY];  
    5.        init();  
    6.    }  

     

    都知道HashMap是个变长的数据结构,看了上面的构造方法可能你并不会认为它有那么神了。

    Java代码   收藏代码
    1. DEFAULT_LOAD_FACTOR   //默认加载因子,如果不制定的话是0.75  

     

    Java代码   收藏代码
    1. DEFAULT_INITIAL_CAPACITY //默认初始化容量,默认是16  

     

    Java代码   收藏代码
    1. threshold //阈(yu)值 根据加载因子和初始化容量计算得出  

        因此我们知道了,如果我们调用无参数的构造方法的话,我们将得到一个16容量的数组

        数组是定长的,如何用一个定长的数据来表示一个不定长的数据呢,答案就是找一个更长的

        下面来看看put方法是怎么实现的

    Java代码   收藏代码
    1. public V put(K key, V value) {  
    2.        if (key == null//键为空的情况,HashMap和HashTable的一个区别  
    3.            return putForNullKey(value);  
    4.        int hash = hash(key.hashCode()); //根据键的hashCode算出hash值  
    5.        int i = indexFor(hash, table.length); //根据hash值算出究竟该放入哪个数组下标中  
    6.        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//整个for循环实现了如果存在K那么就替换V  
    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.    }  

     区区十几行代码,通过我添加的注释看懂并不难,细心的话可能会发现这里并没有体现变长的概念,如果我数据大于之前的容量的怎么继续添加呀,答案就在addEntry方法中

    Java代码   收藏代码
    1. void addEntry(int hash, K key, V value, int bucketIndex) {  
    2. Entry<K,V> e = table[bucketIndex];  
    3.        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    4.        if (size++ >= threshold)  
    5.            resize(2 * table.length);  
    6.    }  

     这里显示了如果当前size>threshold的话那么就会扩展当前的size的两倍,如何扩展?

    Java代码   收藏代码
    1. void resize(int newCapacity) {  
    2.         Entry[] oldTable = table;  
    3.         int oldCapacity = oldTable.length;  
    4.         if (oldCapacity == MAXIMUM_CAPACITY) {  
    5.             threshold = Integer.MAX_VALUE;  
    6.             return;  
    7.         }  
    8.   
    9.         Entry[] newTable = new Entry[newCapacity];  
    10.         transfer(newTable);  
    11.         table = newTable;  
    12.         threshold = (int)(newCapacity * loadFactor);  
    13.     }  

     new 一个新的数组,将旧数据转移到新的数组中,并且重新计算阈值,如何转移数据?

    Java代码   收藏代码
    1. void transfer(Entry[] newTable) {  
    2.        Entry[] src = table;  
    3.        int newCapacity = newTable.length;  
    4.        for (int j = 0; j < src.length; j++) {  
    5.            Entry<K,V> e = src[j];  
    6.            if (e != null) {  
    7.                src[j] = null;  
    8.                do {  
    9.                    Entry<K,V> next = e.next;  
    10.                    int i = indexFor(e.hash, newCapacity);  
    11.                    e.next = newTable[i];  
    12.                    newTable[i] = e;  
    13.                    e = next;  
    14.                } while (e != null);  
    15.            }  
    16.        }  
    17.    }  

     根据hash值,和新的容量重新计算数据下标。天呀,太麻烦了吧。

     到此为止我们知道了新建一个HashMap和添加一个HashMap之后源代码中都干了什么。

         3.hashcode你懂它不

          HashMap是根据hashcode的来进行计算hash值的,equals方法默认也是通过hashcode来进行比较的

          hashCode到底是个什么东西呢?

          我们跟踪JDK源码到Object结果JDK确给了我们一个下面的本地方法

    Java代码   收藏代码
    1. public native int hashCode();  

         通过方法我们只能知道hashcode 是一个int值。

         疑问更加多了,首先它如何保证不同对象的hashcode 值不一样呢,

         既然hashcode是一个整形的,那么它最多的应该只能表示Integer.maxValue个值,

    那么当大于这么多值的情况下这些对象的值又该如何表示呢。

         要理解这些东西需要从操作系统说起了

         //TODO 时间关系,后面再补

     

         4.HashMap的优缺点

         优点:超级快速的查询速度,如果有人问你什么数据结构可以达到O(1)的时间复杂度,没错是HashMap

                 动态的可变长存储数据(和数组相比较而言)

         缺点:需要额外计算一次hash值

                 如果处理不当会占用额外的空间

     

         5.如果更加高效的使用HashMap

           添加

           前面我们知道了添加数据的时候,如果当前数据的个数加上1要大于hashmap的阈值的话,那么数组就会进行一个*2的操作。并且从新计算所有元素的在数组中的位置。

           因此如果我们要添加一个1000个元素的hashMap,如果我们用默认值那么我么需要额外的计算多少次呢

           当大于16*0.75=12的时候,需要从新计算 12次

           当大于16*2*0.75=24的时候,需要额外计算 24次

           ……

           当大于16*n*0.75=768的时候,需要额外计算 768次

           所以我们总共在扩充过程中额外计算12+24+48+……+768次

           因此强力建议我们在项目中如果知道范围的情况下,我们应该手动指定初始大小 像这样

     

    Java代码   收藏代码
    1. Map<Integer,String> maps=new HashMap<Integer,String>(1000);  

     

           删除

           JDK中如下方式进行删除

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

       根据上面代码我们知道了,如果删除是不进行了数组容量的重新定义的。所以,如果你有1000个元素的HashMap就算你最后删除只剩下一个了,你在内存中依然还有大于1000个容量,其中大于999个是空的。 为什么是大于因为扩容之后的HashMap实际容量大于1000个。

     因此如果我们项目中有很大的HashMap,删除之后却很小了,我们还是弄一个新的小的存它 吧。

     

         6.HashMap同步

           如果HashMap在多线程下会出现什么问题呢

           我们知道HashMap不是线程安全的(HashMap和HashTable的另外一个区别),如果我们也想要在多线程的环境下使用它怎么办呢?

           也许你会说不是有HashTable吗?那我们就试试

    Java代码   收藏代码
    1. public class MyThread extends Thread { // 线程类  
    2.     private Map<Integer, String> maps; // 多线程处理的map  
    3.   
    4.     public MyThread(Map<Integer, String> maps) {  
    5.         this.maps = maps;  
    6.     }  
    7.   
    8.     @Override  
    9.     public void run() {  
    10.         int delNumber = (int) (Math.random() * 10000);//随即删除的key  
    11.         op(delNumber);  
    12.     }  
    13.   
    14.     public void op(int delNumber) {  
    15.         Iterator<Map.Entry<Integer, String>> t = maps.entrySet().iterator();  
    16.         while (t.hasNext()) {  
    17.             Map.Entry<Integer, String> entry = t.next();  
    18.             int key = entry.getKey();  
    19.             if (key == delNumber) { //看下key是否是需要删除的key是的话就删除  
    20.                 maps.remove(key);  
    21.                 break;  
    22.             }  
    23.         }  
    24.     }  
    25.   
    26. }  

     

    Java代码   收藏代码
    1. public class HashMapTest {  
    2.     public static void main(String[] args) {  
    3.         testSync();  
    4.     }  
    5.   
    6.     public static void testSync(){  
    7.         Map<Integer, String> maps = new Hashtable<Integer, String>(10000);  
    8. //      Map<Integer, String> maps = new HashMap<Integer, String>(10000);  
    9. //      Map<Integer, String> maps = new ConcurrentHashMap<Integer, String>(10000);  
    10.         for (int i = 0; i < 10000; i++) {  
    11.             maps.put(i, "a");  
    12.         }  
    13.         for(int i=0;i<10;i++){  
    14.             new MyThread(maps).start();  
    15.         }  
    16.     }  
    17.   
    18. }  

     我们使用HashTable来运行试试,不一会就出现了如下错误信息

    Java代码   收藏代码
    1. Exception in thread "Thread-6" java.util.ConcurrentModificationException  
    2.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    3.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    4.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    5. Exception in thread "Thread-4" java.util.ConcurrentModificationException  
    6.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    7.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    8.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    9. Exception in thread "Thread-2" java.util.ConcurrentModificationException  
    10.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    11.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    12.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    13. Exception in thread "Thread-1" java.util.ConcurrentModificationException  
    14.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    15.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    16.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    17. Exception in thread "Thread-8" java.util.ConcurrentModificationException  
    18.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    19.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    20.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    21. Exception in thread "Thread-9" java.util.ConcurrentModificationException  
    22.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    23.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    24.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    25. Exception in thread "Thread-5" java.util.ConcurrentModificationException  
    26.     at java.util.Hashtable$Enumerator.next(Hashtable.java:1031)  
    27.     at cn.tang.demos.hashmap.MyThread.op(MyThread.java:22)  
    28.     at cn.tang.demos.hashmap.MyThread.run(MyThread.java:16)  
    29. ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2  
    30. JDWP exit error AGENT_ERROR_NO_JNI_ENV(183):  [../../../src/share/back/util.c:820]  

     不是说是安全的不?为什么会出现这个问题呢,继续看源代码

    Java代码   收藏代码
    1. public T next() {  
    2.         if (modCount != expectedModCount)  
    3.         throw new ConcurrentModificationException();  
    4.         return nextElement();  
    5.     }  

      当修改之后的计数器和期望的不一致的时候就会抛出异常了。对应于上面代码,线程1,遍历的时候假如有100个,本来删除之后就99个,但是线程2这段时间也删除了一个

    所以实际只有98个了,线程1并不知道,当线程1调用next方法时候比较下结果不对,完了,数据不对了,老板要扣工资了,线程自己也解决不了,抛出去吧,别引起更大的问题了。

    于是你得到了一个ConcurrentModificationException。

    所以以后要注意了,HashTable,vector都不是绝对线程安全的了,所以我们需要将maps加上同步

    Java代码   收藏代码
    1. public void op(int delNumber) {  
    2.         synchronized (maps) {  
    3.             Iterator<Map.Entry<Integer, String>> t = maps.entrySet().iterator();  
    4.             while (t.hasNext()) {  
    5.                 Map.Entry<Integer, String> entry = t.next();  
    6.                 int key = entry.getKey();  
    7.                 if (key == delNumber) { // 看下key是否是需要删除的key是的话就删除  
    8.                     maps.remove(key);  
    9.                     break;  
    10.                 }  
    11.             }  
    12.         }  
    13.     }  

     synchronized(maps)加上之后就不会出现问题了,就算你用的是HashMap都不会出问题。

    其实JDK中在早在1.5之后又了ConcurrentHashMap了这个类你可以放心的在多线程下使用并且不需要加任何同步 了。

  • 相关阅读:
    我的20130220日 在北京 do{ Web Develop } while(!die)
    关于NVelocity模板引擎初学总结
    C# 23种设计模式汇总
    基于模型的设备故障检测
    图像去噪之光斑去除
    虹膜识别
    封闭曲线拟合
    基于故障树的系统可靠性分析
    图像识别之棋子识别
    时间序列的模式距离
  • 原文地址:https://www.cnblogs.com/james1207/p/3367623.html
Copyright © 2020-2023  润新知