首页 > 技术文章 > 深入理解-HashMap

plxx 2015-07-13 13:16 原文

一、HashMap概述

  HashMap 在家族中位置:实现了Map接口,继承AbstractMap类。HashMap 允许key/value 都为null.

二、HashMap存储结构

  

  HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。在其内部维护一个Entry类型数组,初始大小为16。

 1 /**
 2  * The table, resized as necessary. Length MUST Always be a power of two.
 3  */
 4 transient Entry[] table;
 5 
 6 static class Entry<K,V> implements Map.Entry<K,V> {
 7     final K key;
 8     V value;
 9     Entry<K,V> next;
10     final int hash;
11     ……
12 }

三、HashMap 的存取遍历

  1、存储 

 1 public V put(K key, V value) {
 2     // HashMap允许存放null键和null值。
 3     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
 4     if (key == null)
 5         return putForNullKey(value);
 6     // 根据key的keyCode重新计算hash值。
 7     int hash = hash(key.hashCode());
 8     // 搜索指定hash值在对应table中的索引。
 9     int i = indexFor(hash, table.length);
10     // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
11     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
12         Object k;
13         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
14             V oldValue = e.value;
15             e.value = value;
16             e.recordAccess(this);
17             return oldValue;
18         }
19     }
20     // 如果i索引处的Entry为null,表明此处还没有Entry。
21     modCount++;
22     // 将key、value添加到i索引处。
23     addEntry(hash, key, value, i);
24     return null;
25 }

当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

 1     final int hash(Object k) {
 2         int h = hashSeed;
 3         if (0 != h && k instanceof String) {
 4             return sun.misc.Hashing.stringHash32((String) k);
 5         }
 6 
 7         h ^= k.hashCode();
 8 
 9         // This function ensures that hashCodes that differ only by
10         // constant multiples at each bit position have a bounded
11         // number of collisions (approximately 8 at default load factor).
12         h ^= (h >>> 20) ^ (h >>> 12);
13         return h ^ (h >>> 7) ^ (h >>> 4);
14     }
1     static int indexFor(int h, int length) {
2         // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
3         return h & (length-1);
4     }

  获取的hash码之后对数组大小取模。

   addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组table的i索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:

1     void addEntry(int hash, K key, V value, int bucketIndex) {
2         if ((size >= threshold) && (null != table[bucketIndex])) {
3             resize(2 * table.length);
4             hash = (null != key) ? hash(key) : 0;
5             bucketIndex = indexFor(hash, table.length);
6         }
7 
8         createEntry(hash, key, value, bucketIndex);
9     }

如果经过hash之后,数组的下标不在数组大小范围之内而且当前数组大小超过指定阈值,那么将会把hashmap 2倍扩容。

2、读取数据  

1     public V get(Object key) {
2         if (key == null)
3             return getForNullKey();
4         Entry<K,V> entry = getEntry(key);
5 
6         return null == entry ? null : entry.getValue();
7     }
 1     private V getForNullKey() {
 2         if (size == 0) {
 3             return null;
 4         }
 5         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 6             if (e.key == null)
 7                 return e.value;
 8         }
 9         return null;
10     }
 1 final Entry<K,V> getEntry(Object key) {
 2         if (size == 0) {
 3             return null;
 4         }
 5 
 6         int hash = (key == null) ? 0 : hash(key);
 7         for (Entry<K,V> e = table[indexFor(hash, table.length)];
 8              e != null;
 9              e = e.next) {
10             Object k;
11             if (e.hash == hash &&
12                 ((k = e.key) == key || (key != null && key.equals(k))))
13                 return e;
14         }
15         return null;
16     }

  首先,如果要获取的key==null,那么直接调用getForNullKey()从table[0]中,获取相应的value;如果key != null,那么将会获得该key的hashcode,之后获取table的下标,从中遍历出来相应的value。

3、遍历  

1         Set<String> keys = map.keySet();
2         Iterator<String> iterator = keys.iterator();
3         while (iterator.hasNext()) {
4             String key = iterator.next();
5             System.out.println("[" + key + "," + map.get(key) + "]");
6         }

4、resize() 再哈希

  当哈希表的容量超过默认容量时(在hashmap中,如果第一次hash之后,找到的数组下标不在合理的范围之内而且当前的大小超过规定阈值),必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。

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

 

loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

四、性能

  Fail-Fast机制

  我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略

这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。其中modCount是声明为volatile,保证线程可见。

 

感谢:http://zhangshixi.iteye.com/blog/672697

 

推荐阅读