Java中HashMap实现原理初探

http://blog.chinaunix.net/uid-11775320-id-3143919.html中,对HashMap有了大致的了解:

1.    HashMap概述

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

 

2.    HashMap的数据结构

   java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

据结构都可以用这两个基本结构来构造的, HashMap 也不例外。 HashMap 实际上是一个 链表散列 的数据结构,即数组和链表的结合体。

3.            HashMap的存取 
    HashMap的功能是通过“键(key)”能够快速的找到“值”。下面我们分析下HashMap存数据的基本流程: 
    1、 当调用put(key,value)时,首先获取key的hashcode,int hash = key.hashCode(); 
    2、 再把hash通过一下运算得到一个int h. 
hash ^= (hash >>> 20) ^ (hash >>> 12); 
int h = hash ^ (hash >>> 7) ^ (hash >>> 4); 
为什么要经过这样的运算呢?这就是HashMap的高明之处。先看个例子,一个十进制数32768(二进制1000 0000 0000 0000),经过上述公式运算之后的结果是35080(二进制1000 1001 0000 1000)。看出来了吗?或许这样还看不出什么,再举个数字61440(二进制1111 0000 0000 0000),运算结果是65263(二进制1111 1110 1110 1111),现在应该很明显了,它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分布。那这样有什么意义呢?看第3步。 
    3、 得到h之后,把h与HashMap的承载量(HashMap的默认承载量length是16,可以自动变长。在构造HashMap的时候也可以指定一个长 度。这个承载量就是上图所描述的数组的长度。)进行逻辑与运算,即 h & (length-1),这样得到的结果就是一个比length小的正数,我们把这个值叫做index。其实这个index就是索引将要插入的值在数组中的 位置。第2步那个算法的意义就是希望能够得出均匀的index,这是HashTable的改进,HashTable中的算法只是把key的 hashcode与length相除取余,即hash % length,这样有可能会造成index分布不均匀。还有一点需要说明,HashMap的键可以为null,它的值是放在数组的第一个位置。 
    4、 我们用table[index]表示已经找到的元素需要存储的位置。先判断该位置上有没有元素(这个元素是HashMap内部定义的一个类Entity, 基本结构它包含三个类,key,value和指向下一个Entity的next),没有的话就创建一个Entity对象,在 table[index]位置上插入,这样插入结束;如果有的话,通过链表的遍历方式去逐个遍历,看看有没有已经存在的key,有的话用新的value替 换老的value;如果没有,则在table[index]插入该Entity,把原来在table[index]位置上的Entity赋值给新的 Entity的next,这样插入结束。 
总结:keyàhashcodeàhàindexà遍历链表à插入

另外:关于>>>的理解,http://blog.csdn.net/yin2na/article/details/6250694讲解的比较好

了解大致后,查看具体源代码:

先看put的源代码:(参考了http://alex09.iteye.com/blog/539545)

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry 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;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

从上面源码可以看出,put的时候先根据key的hashcode算出hash值,再得到在数组中的存储位置下标,开始检查该位置上是否已有Entry,如果有,则遍历检查是否有与当前要加入的Entry的key值相同的Entry,如果有,则用当前要插入的Entry的value覆盖原有的value,如果没有,就遍历到索引处链表尾部插入Entry。(保证key的唯一性)


 if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
这里我一开始不理解为什么要比较hash而不能直接比较key值,后来参考了http://blog.csdn.net/iceman1952/article/details/7358716 得知两个对象相等的前提是他们的hashcode相等,所以必须要先判断hash值是否相等,但这只是必要条件,所以还要判断key值是否相等。但是根据hash值得算法,我认为key相等必然会得出相同的hash值(这里仍待考证,hashcode是如何得到的还需研究),还是觉得判断hash是否相等没有必要,后来发现是为了效率,如果判断hash不想等,那么必然key不相等,则无需进行后面的equals判断,提高了效率。

addEntry方法源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
在addEntry中首先判断了是否需要扩容,这里有两个变量需要了解:size和threshold。

size是当前hashmap中存储的key-value对的个数;

threshold 的算法:capacity * load factor(capacity为数组table的大小,loadfactor为负载因子)

关于负载因子的理解(默认0.75):当HashMap中的数据量/HashMap的总容量(initialCapacity)达到指定值或者0.75时候,HashMap的总容量自动扩展一倍,以此类推。

上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。 

resize的源代码:

 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, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

resize扩容的方法是重建了一个新的数组,并将原来table中的所有元素重新计算位置填入新的table中(由transfer完成):

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry e : table) {
            while(null != e) {
                Entry next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

这里遍历旧table中的所有元素,重新根据新的数组容量(newCapacity)算出存储位置放入新的table中。

接下来看get方法:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
final Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry 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;
    }

原理就是根据key算出hash再算出位置,遍历索引位置处链表,如果有则取出Entry,再返回Entry的value值。

remove方法:

 

 public V remove(Object key) {
        Entry e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
 final Entry removeMapping(Object o) {
        if (size == 0 || !(o instanceof Map.Entry))
            return null;

        Map.Entry entry = (Map.Entry) o;
        Object key = entry.getKey();
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry prev = table[i];
        Entry e = prev;

        while (e != null) {
            Entry next = e.next;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }


final Entry removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry prev = table[i];
        Entry e = prev;

        while (e != null) {
            Entry next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
原理都大同小异,找到相应元素后删除,主要使用到了链表中的删除。

最后有关于性能,http://alex09.iteye.com/blog/539545中提到:


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

接下来还要看HashSet,Collection框架等内容





你可能感兴趣的:(Java中HashMap实现原理初探)