zoukankan      html  css  js  c++  java
  • 由JDK源码学习HashMap

      HashMap基于hash表的Map接口实现,它实现了Map接口中的所有操作。HashMap允许存储null键和null值。这是它与Hashtable的区别之一(另外一个区别是Hashtable是线程安全的)。另外,HashMap中的键值对是无序的。下面,我们从HashMap的源代码来分析HashMap的实现,以下使用的是Jdk1.7.0_51。

     一、HashMap的存储实现

      HashMap底层采用的是数组和链表这两种数据结构.当我们把key-value对put到HashMap时,系统会根据hash算法计算key的hash值,根据hash值决定key-value对存放在数组的哪个位置(也就是散列表中的”桶”位).如果该位置已经存放Entry,则该位置上的Entry形成Entry链.下面我们从源代码入手分析.

    public V put(K key, V value) {
            //① 如果table为空,调用inflateTable()初始化table数组
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            // 如果key为null,调用putForNullKey()处理
            if (key == null)
                return putForNullKey(value);
            // ② 调用hash算法,算出key的hash值
            int hash = hash(key);
            // ③ 根据hash值和table的长度计算在table中的存放位置
            int i = indexFor(hash, table.length);
            // 如果key存在,则替换之前的value值
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            // 模数自增,用于实现fail-fast机制
            modCount++;
            // ④ 添加key-value对
            addEntry(hash, key, value, i);
            return null;
    }

      上面的程序中用到了一个重要的内部接口:Map.Entry,每个Map.Entry其实就是一个封装了key-value属性的对象.从上面的代码中也可以看出:系统决定HashMap中的key-value对时,没有考虑Entry中的value,仅仅是根据key来计算并决定每个Entry的存储位置.

      从①处代码可以看到,调用put方法时会检查table数组的容量.如果table数组为空数组,会先初始化table数组,我们看下HashMap是如何初始化table数组的。

    private void inflateTable(int toSize) {
            // 找到大于toSize的最小的2的n次方
            int capacity = roundUpToPowerOf2(toSize);
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            // 初始化table数组
            table = new Entry[capacity];
            initHashSeedAsNeeded(capacity);
        }

      从上面的代码中可以知道,HashMap中table数组的长度一定是2的n次方.实际上,这是一个很优雅的设计,在后面我们还会提到。如果key不为null,系统会调用hash()算法算出key的hash值,并据此来计算key的的存放位置.

    final int hash(Object k) {
            int h = hashSeed;
            // 如果key为字符串,调用stringHash32()处理
            // 因为字符串的hashCode码一样的可能性大,造成hash冲突的可能性也大
            if (0 != h && k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            // 根据key的hashCode值算hash值
            h ^= k.hashCode();
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

      得到key的hash值后,从④处的代码知道,此时系统会根据hash值和table的长度来计算key在table数组中的存放位置.

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

      这个方法的设计非常巧妙,通过h&(table.length-1)来得到该key的保存位置,而上面说到了HashMap底层数组长度总是2的n次方.当length总是2的n次方时,h&(length-1)能保证计算得到的值总是位于table数组的索引之内.假设h=5,length=16,h&(length-1)=5;h=6,length=16,h&(length-1)=6…

      接下来,如果key已经存在,则替换其value值.如果不存在则调用addEntry()处理.

    void addEntry(int hash, K key, V value, int bucketIndex) {
            // 检查HashMap容量是否达到极限(threshold)值
            if ((size >= threshold) && (null != table[bucketIndex])) {
                // 扩充table数组的容量为之前的1倍
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
            // 调用createEntry()添加key-value对到HahsMap
            createEntry(hash, key, value, bucketIndex);
        }
        void createEntry(int hash, K key, V value, int bucketIndex) {
            // 获取table[bucketIndex]的Entry
            Entry<K,V> e = table[bucketIndex];
            // 根据key-value创建新的Entry对象,并把新创建的Entry存放到table[bucketIndex]处
            // 新Entry对象保存e对象(之前table[bucketIndex]的Entry对象)的引用,从而形成Entry链
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }

      系统总是将新添加的Entry对象放入table[bucketIndex]—如果bucketIndex处已经有一个Entry对象,那新添加的Entry对象指向原有的Entry对象(Entry持有一个指向原Entry对象的引用,产生一个Entry链),如果bucketIndex处没有Entry对象,即上面代码中e为null,也就是新添加的Entry对象持有一个null引用,并没有产生Entry链.

      从上面整个put方法的分析来看,我们可以知道HashMap存储元素的基本流程:首先根据算出key的hash值,根据hash值和table的长度计算该key的存放位置.如果key相同,则新值替换旧值.如果key不同,则在table[i]桶位形成Entry链,而且新添加的Entry位于Entry链的头部(table[i]).

      上面的代码有点多,附上put(K key,V value)方法的流程图:

    下面是HashMap的存储示意图:

      

    二、HashMap的读取实现

      当HashMap的每个buckete里存储的Entry只是单个Entry—也就是没有通过指针产生Entry链(没有产生hash冲突)时,此时HashMap具有最好的性能(底层结构仅仅是数组,没有产生链表):当程序通过key取出对应的value时,系统先计算出hash(key)值找到key在table数组的存放位置,然后取出该桶位的Entry链,遍历找到key对应的value.以下是get(K key)方法的源代码:

    public V get(Object key) {
            // 如果key为null,调用getForNullKey()处理
            if (key == null)
                return getForNullKey();
            // 获取key所对应的Entry
            Entry<K,V> entry = getEntry(key);
    
            return null == entry ? null : entry.getValue();
        }
    final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
            // 计算hash(key)值
            int hash = (key == null) ? 0 : hash(key);
            // 遍历Entry链,找到key所对应的Entry
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }

      上面的代码很简单,如果HashMap的每个bucket里只有一个Entry时,HashMap可以根据hash(key)值快速取出table[bucket]的Entry.在发生”Hash冲突”的情况下,table[bucket]存放的不是一个Entry,而是一个Entry链,系统只能按顺序遍历Entry链,直到找到key相等的Entry,如果要搜索的Entry位于Entry链的最末端(该Entry最早放入bucket),那么系统必须循环到最后才能找到该Entry.

      归纳起来简单地说,HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象.HashMap底层采用一个Entry[]数组来保存所有的key-value对,当存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,再取出该Entry.由此可见:HashMap快速存取的基本原理是:不同的东西放在不同的位置,需要时才能快速找到它.

    三、Hash算法的性能选项 

    HashMap中定义了以下几个成员变量:

      ① size:HashMap中存放的Entry数量

      ② loadFactor:HashMap的负载因子

      ③  threshold:HashMap的极限容量,当HashMap的容量达到该值时,HashMap会自动扩容(threshold=loadFactory*table.length)

      HashMap默认的构造函数会创建一个初始容量为16,负载因子为0.75的HashMap对象.当然,我们也可以通过其他构造函数指定HashMap的初始容量和负载因子.从上面的源码分析中,我们知道创建HashMap时的实际容量通常比initialCapacity大一些,除非我们指定的initialCapacity参数值正好是2的n次方.当然,知道这个以后,应该在创建HashMap时将initialCapacity参数值指定为2的n次方,这样可以减少系统的计算开销.

      当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75,这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表(Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询时最频繁的操作(HashMap的get()与put()方法都要用到查询);减少负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间.

    如果能够预估HashMap会保存Entry的数量,可以再创建HashMap时指定初始容量,如果HashMap的size一直不会超过threshold(capacity*loadFactory),就无需调用resize()重新分配table数组,resize()是很耗性能的,因为要对所有的Entry重新分配位置.当然,开始就将初始化容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap时初始化容量也需要小心设置.

    四、细数HashMap中的优雅的设计

    1. 底层数组的长度总是为2的n次方
    2. indexFor(hash,table.length)保证每个Entry的存储位置都在table数组的长度范围内
    3. 新添加的Entry总是存放在table[bucket],相同hash(key)的Entry形成Entry

      目前就发现这么多,以后发现了再继续补上.

      都说好的设计是成功的一半,HashMap的设计者展示了一种设计美感.

    五、HashMap使用注意问题

      以本人目前的经验来看,HashMap使用过程中应注意两大类问题,其一,线程安全问题,因为HashMap是非同步的,在多线程情况下请使用ConcurrentHashMap。其二,内存泄露问题.我们这里只讨论第二种问题.由上面的分析可以知道,存放到HashMap的对象,强烈建议覆写equals()和hashCode().但hashCode值的改变可能会造成内存泄露问题.看代码:

    public class HashCodeDemo {
        public static void main(String[] args) {
            User user = new User("zhangsan",22);
            Map<User,Object> map = new HashMap<User,Object>();
            map.put(user, "user is exists");
            // user is exists
            System.out.println(map.get(user));
            // 改变age值,将会改变hashCode值
            user.setAge(23);
            // null,因为user.hashCode值变化了,此时,我们可能永远也无法取出该Entry对象,但HashMap持有该Entry对象的引用,这就造成了内存泄露
            System.out.println(map.get(user));
        }
    }
    
    class User{
        private String name;
        private Integer age;
        
        public User() {
        }
        public User(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Integer getAge() {
            return age;
        }
        public void setAge(Integer age) {
            this.age = age;
        }
        @Override
        public int hashCode() {
            return name.hashCode()*age.hashCode();
        }
        @Override
        public boolean equals(Object obj) {
            if(obj instanceof User){
                User user = (User)obj;
                return this.name.equals(user.name)&&this.age==user.getAge()?true:false;
            }
            return false;
        }
    }

    六、自定义HashMap实现

      这里只做简单模拟,加深对HashMap的理解.  

      第一步,创建MyEntry类,用于封装key-value属性.

    class MyEntry<K, V> {
        private final K key;
        private V value;
        private MyEntry<K, V> next;
        private final int hash;
    
        /** 构造函数 **/
        public MyEntry(K key, V value, MyEntry<K, V> next, int hash) {
            this.key = key;
            this.value = value;
            this.next = next;
            this.hash = hash;
        }
    
        /** 返回Entry.key **/
        public K getKey() {
            return this.key;
        }
    
        /** 返回Entry.value **/
        public V getValue() {
            return this.value;
        }
    
        /** 替换Entry.value **/
        public V setValue(V val) {
            V oldVal = value;
            this.value = val;
            return oldVal;
        }
        public MyEntry next(){
            return next;
        }
        public int hash(){
            return hash;
        }
        @Override
        public String toString() {
            return this.key + "=" + this.value;
        }
    
        public void setNext(MyEntry myEntry) {
            this.next = myEntry;
            
        }
    }

    第二步,实现MyHashMap,底层采用数组+链表结构.到这里,我们会发现,其实实现HashMap关键点有以下几个:

      ① HashMap容量的管理和性能参数的设置

          ② hash()算法的实现,理想的hash算法是不会产生"hash冲突的"(HashMap底层仅仅是数组),在这种情况下,HashMap能达到最好的存取性能.

      HashMap的设计者很好的解决了这两个问题,关于这两个问题,可以参考源码.

    以上就是我对HashMap源码的学习总结,有不正确或不准确的地方,请大家指出来!非常欢迎大家一起交流学习!

    以上内容参考:http://www.ibm.com/developerworks/cn/java/j-lo-hash/?ca=drs-tp4608

        

  • 相关阅读:
    C#中的global::system***命名空间别名限定符
    返回一个整数数组中最大子数组的和
    敏捷开发概述
    单词查找排序输出
    关于电梯调度的设计
    关于电梯调度的一些想法
    C#中抽象类和接口的区别
    SharePoint2010列表表单:用后台代码生成表单
    外刊IT评论:远离.net
    程序员:编程给你现实生活带来了哪些坏习惯
  • 原文地址:https://www.cnblogs.com/heavenyes/p/3787780.html
Copyright © 2011-2022 走看看