由JDK源码学习HashMap

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

 1、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数组中的存放位置.ide

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的存储示意图:

  

2、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快速存取的基本原理是:不一样的东西放在不一样的位置,须要时才能快速找到它.

3、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时初始化容量也须要当心设置.

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

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

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

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

5、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;
    }
}

 

6、自定义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

相关文章
相关标签/搜索