HashMap原理解析

日常工作中经常用到MAP,基本上是用Map map=new HashMap()来得到一个HashMap对象,之前并未深入去研究HashMap的实现原理,只是去简单的去创建然后使用它。

这次想深入了解便,去研究了一下HashMap的源码。

做点笔记,记录一下自己的一些收获,想到哪写到哪吧。

HashMap继承自AbstractMap类并实现了Map、Cloneable、Serializable等接口。

HashMap是一种链表散列的数据结构。

链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

散列表(Hash表):根据关键码(key-value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

new一个HashMap对象时创建了一个Entry[] table数组, Entry是HashMap的内部类。Entry[] table默认初始化长度为16。

Entry[] table中的某一个元素及其对应的Entry<Key,Value>又被称为桶(bucket)

其结构如下图所示:

 

以上是HashMap的内部组织结构图。

 HashMap的算法实现解析

     我们主要解释一下HashMap的put方法:

 

/**
     * 将<Key,Value>键值对存到HashMap中,如果Key在HashMap中已经存在,那么最终返回被替换掉的Value值。
     * Key 和Value允许为空
     */
    public V put(K key, V value) {
        
    //1.如果key为null,那么将此value放置到table[0],即第一个桶中
    if (key == null)
            return putForNullKey(value);
    //2.重新计算hashcode值,
        int hash = hash(key.hashCode());
        //3.计算当前hashcode值应当被分配到哪一个桶中,获取桶的索引
        int i = indexFor(hash, table.length);
        //4.循环遍历该桶中的Entry列表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //5. 查找Entry<Key,Value>链表中是否已经有了以Key值为Key存储的Entry<Key,Value>对象,
            //已经存在,则将Value值覆盖到对应的Entry<Key,Value>对象节点上
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//请读者注意这个判定条件,非常重要!!!
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //6不存在,则根据键值对<Key,Value> 创建一个新的Entry<Key,Value>对象,然后添加到这个桶的Entry<Key,Value>链表的头部。
        addEntry(hash, key, value, i);
        return null;
    }
 
    /**
     * Key 为null,则将Entry<null,Value>放置到第一桶table[0]中
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
 
    }

 

    /**
     * 根据特定的hashcode 重新计算hash值,
     * 由于JVM生成的的hashcode的低字节(lower bits)冲突概率大,(JDK只是这么一说,至于为什么我也不清楚)
     * 为了提高性能,HashMap对Key的hashcode再加工,取Key的hashcode的高字节参与运算
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * 返回此hashcode应当分配到的桶的索引
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
 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);
    }
    
    /**
     * Transfers all entries from current table to newTable.
     */
    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);
            }
        }
    }

(1)HashMap允许key为空值,当key为空值时,会把key-value键值对放到table[0];

(2)然后计算出Key的hash值并得到它的精确索引地址;

(3)根据索引值定位到table中桶(bucket)的位置,遍历索引处的Entry链表,若hash值相同且key相同,则用新的Value替换老的Value,并返回oldValue;

(4)若不存在相同key,则调用addEntry()方法在索引处新增Entry对象,并放置在当前位置Entry链表的头部。然后判断size是否超过了阀值。若超过阀值则调用resize(int capacity)方法。

(5)HashMap的put方法里会调用一个addEntry的方法,这可能会导致数组长度超过阀值(table数组长度*加载因子),加载因子的经验值为0.75,为了节省空间我们可以增大加载因子,但时间复杂度会增大。

(6)HashMap的默认初始长度为2的4次方,且每次扩充都是在原有的基础上乘2,之前一直对为什么长度要是2的次方迷惑,经过查阅后得到了答案。

Put方法里有一个IndexFor方法会用位运算符(&)比较key的hash值(h)与table的length-1,&比较的是二进制数(当参数是布尔型的时候和逻辑运算符&&有相同的作用只是没有短路功能),当h<length-1,取h值;当h>=length-1,取length-1,当length为2的n次幂时,length-1的2进制位都为1,与h做与运算,能最大程度利用空间,减少冲突。位运算符(&):当对应位置都为1的时候才为1。

(7)我们尽可能少的增加链表的复杂度,因为时间的复杂度为O(n),而空间的复杂度为O(1),理想的状态时每个索引的位置之储存一个Key-Value,这样检索的效率是最高的,但这样消耗的空间也是最大的。

(8)因为数组在内存中占用连续存储的空间,所以扩充的时候得重新定义数组,并将原Entry[] table打散重新计算hash值后均匀的分布到新的数组中,这里要消耗很多的性能,所以我们最好在定义的时候大致确定好数组的长度。

 

以上是一些总结,如果想要了解更详细的,可以看这篇文章

http://blog.csdn.net/luanlouis/article/details/41576373?utm_source=tuicool&utm_medium=referral#0-qzone-1-52069-d020d2d2a4e8d1a374a433f596ad1440

 

猜你喜欢

转载自zg0621.iteye.com/blog/2307056