一、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