概述
HashMap是一个散列表,是基于拉链法实现的。这个类继承了Map接口,Map接口提供了所有的哈希操作,比如set()、put()、remove()等,并且允许操作的键值对为null。HashMap跟Hashtable基本相同,区别是HashMap是非同步的并且允许键值对为null。HashMap不保证映射的顺序,特别是不保证该顺序恒久不变。
在用到的哈希函数均匀性比较好的前提下,基本操作比如put和get的时间都是O(1)的。处理哈希冲突所需的时间与哈希表的容量(桶的数目)加上表的大小(存储的键值映射对)之和成正比。所以在初始化哈希表时,初始容量不要太大(装载因子不要太小)。
影响HashMap性能的两个比较重要的参数是初始容量(initial capacity)和装载因子(load factor)。容量(capacity)是哈希表中桶的数量,初始容量就是哈希表创建时桶的数量。装载因子(load factor)是一个参数,它用来衡量哈希表满和空的程度,它在公式上等于size(键值对数量)/capacity(容量)。当装载因子大于某个值时,哈希表会进行重构(rehash),一般会将桶的数目加倍,哈希表的初始容量一般设为2的幂。
负载因子一般设为0.75。这是时间费用和空间费用的比较好的权衡。负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
HashMap是非同步的,如果有多个线程并发的的访问,并且只是有个线程在结构上改变哈希表,必须增加额外的同步策略。这里所说的在结构上该表哈希表是指增加或删除一个或多个键值对,只是改变某个关键字所对应的值并不是指改变哈希表结构。
HashMap的数据结构
在Java编程语言中,最基本的结构就是两种,一个是数组,一个是模拟指针(引用),所有的数据结构都可以用这两个结构来构造,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。这是因为HashMap使用拉链法来解决哈希冲突的。
从构造函数可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 final int hash; 6 7 ........ 8 }
Entry就是数组中的元素,每个Map.Entry其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。同时,hash用来计算桶号,每一个桶就是一个链表。
源码阅读
HashMap类的源代码主要分如下及部分:
1)HashMap自身的成员变量及方法;
2)Entry的成员及方法。Entry是HashMap的子类,哈希表是一个Entry数组,数组的每一项代表一个桶;
3)
1.类成员变量
/** *哈希表默认的初始容量 ,必须是2的幂. */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 哈希表的最大容量。 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 转载因子,此值用于无参数的构造函数。 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** *哈希表,需要时会调整大小,长度必须是2的幂. */ transient Entry[] table; /** * 哈希表中的键值对. */ transient int size; /** * 阈值,当哈希表键值对数目达到此值时,哈希表会调整大小,等于capacity * load factor. * @serial */ int threshold; /** * 哈希表的转载因子. * * @serial */ final float loadFactor; /** * 哈希表结构改变的次数 */ transient volatile int modCount;
如以上源码所示,定义了哈希表重要的参数。因为HashMap实现了Serilizable接口,对象可以序列化,被关键字transient声明的变量可以不被序列化,详情可以参考http://www.cnblogs.com/lanxuezaipiao/p/3369962.html。
2构造函数
HashMap有四个构造函数,
1 /** 2 * Constructs an empty <tt>HashMap</tt> with the specified initial 3 * capacity and load factor. 4 * 5 * @param initialCapacity the initial capacity 6 * @param loadFactor the load factor 7 * @throws IllegalArgumentException if the initial capacity is negative 8 * or the load factor is nonpositive 9 */ 10 public HashMap(int initialCapacity, float loadFactor) { 11 if (initialCapacity < 0) 12 throw new IllegalArgumentException("Illegal initial capacity: " + 13 initialCapacity); 14 if (initialCapacity > MAXIMUM_CAPACITY) 15 initialCapacity = MAXIMUM_CAPACITY; 16 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 17 throw new IllegalArgumentException("Illegal load factor: " + 18 loadFactor); 19 20 // Find a power of 2 >= initialCapacity 21 int capacity = 1; 22 while (capacity < initialCapacity) 23 capacity <<= 1; 24 25 this.loadFactor = loadFactor; 26 threshold = (int)(capacity * loadFactor); 27 table = new Entry[capacity]; 28 init(); 29 } 30 31 /** 32 * Constructs an empty <tt>HashMap</tt> with the specified initial 33 * capacity and the default load factor (0.75). 34 * 35 * @param initialCapacity the initial capacity. 36 * @throws IllegalArgumentException if the initial capacity is negative. 37 */ 38 public HashMap(int initialCapacity) { 39 this(initialCapacity, DEFAULT_LOAD_FACTOR); 40 } 41 42 /** 43 * Constructs an empty <tt>HashMap</tt> with the default initial capacity 44 * (16) and the default load factor (0.75). 45 */ 46 public HashMap() { 47 this.loadFactor = DEFAULT_LOAD_FACTOR; 48 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); 49 table = new Entry[DEFAULT_INITIAL_CAPACITY]; 50 init(); 51 } 52 53 /** 54 * Constructs a new <tt>HashMap</tt> with the same mappings as the 55 * specified <tt>Map</tt>. The <tt>HashMap</tt> is created with 56 * default load factor (0.75) and an initial capacity sufficient to 57 * hold the mappings in the specified <tt>Map</tt>. 58 * 59 * @param m the map whose mappings are to be placed in this map 60 * @throws NullPointerException if the specified map is null 61 */ 62 public HashMap(Map<? extends K, ? extends V> m) { 63 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, 64 DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); 65 putAllForCreate(m); 66 }
构造函数的重要功能是初始化capacity、loadFactor、threshold和table等。值得注意的是,第四个构造函数是根据已有的某个HashMap创建新的HashMap.
3基本哈希操作
3.1求索引
1 /** 2 * Applies a supplemental hash function to a given hashCode, which 3 * defends against poor quality hash functions. This is critical 4 * because HashMap uses power-of-two length hash tables, that 5 * otherwise encounter collisions for hashCodes that do not differ 6 * in lower bits. Note: Null keys always map to hash 0, thus index 0. 7 */ 8 static int hash(int h) { 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 } 15 16 /** 17 * Returns index for hash code h. 18 */ 19 static int indexFor(int h, int length) { 20 return h & (length-1); 21 }
这两个函数是求索引的,也就是键值对应该存放在哪个桶中。hash函数的输入是key的hashcode()。
3.2存
1 /** 2 * Associates the specified value with the specified key in this map. 3 * If the map previously contained a mapping for the key, the old 4 * value is replaced. 5 * 6 * @param key key with which the specified value is to be associated 7 * @param value value to be associated with the specified key 8 * @return the previous value associated with <tt>key</tt>, or 9 * <tt>null</tt> if there was no mapping for <tt>key</tt>. 10 * (A <tt>null</tt> return can also indicate that the map 11 * previously associated <tt>null</tt> with <tt>key</tt>.) 12 */ 13 public V put(K key, V value) { 14 if (key == null) 15 return putForNullKey(value); 16 int hash = hash(key.hashCode()); 17 int i = indexFor(hash, table.length); 18 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 19 Object k; 20 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 21 V oldValue = e.value; 22 e.value = value; 23 e.recordAccess(this); 24 return oldValue; 25 } 26 } 27 28 modCount++; 29 addEntry(hash, key, value, i); 30 return null; 31 }
当我们向HashMap中put元素的时候,先根据key的hashcode作为hash函数的输入计算hash值,根据hash值得到这个元素在数组中的下标,也就是哈希表的桶号。如果数组该位置上已经存放有其他元素,那么将新加入的放在链表头。如果数组该位置上没有元素,就直接将该元素放到数组中的该位置上。
分为三个情况:1)key已存在HashMap中;2)key不存在;3)key为null。对于第一种情况,定位到这个key,并将旧的value替换为新的value。对于第二种情况,调用addEntry函数,定义如下
1 void addEntry(int hash, K key, V value, int bucketIndex) { 2 Entry<K,V> e = table[bucketIndex]; 3 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 4 if (size++ >= threshold) 5 resize(2 * table.length); 6 }
addEntry函数调用了Entry的构造函数,如下
1 Entry(int h, K k, V v, Entry<K,V> n) { 2 value = v; 3 next = n; 4 key = k; 5 hash = h; 6 }
从代码中可以看到,向表中插入一个Entry时,用到的是前插法,也就是说,新的Entry放在了链表的头部。当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。第三种情况,key为null时,调用函数putForNullKey,函数如下:
1 private V putForNullKey(V value) { 2 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 3 if (e.key == null) { 4 V oldValue = e.value; 5 e.value = value; 6 e.recordAccess(this); 7 return oldValue; 8 } 9 } 10 modCount++; 11 addEntry(0, null, value, 0); 12 return null; 13 }
当key为null时,索引为0.
1 static int hash(int h) { 2 // This function ensures that hashCodes that differ only by 3 // constant multiples at each bit position have a bounded 4 // number of collisions (approximately 8 at default load factor). 5 h ^= (h >>> 20) ^ (h >>> 12); 6 return h ^ (h >>> 7) ^ (h >>> 4); 7 }
hash(int h)方法根据key的hashcode重新计算一次散列,此算法加入了高位计算,防止当低位不变,高位变化时,造成hash冲突。
在HashMap中要找到某个元素时,需要根据key的hash值来求得对应数组中的位置。HashMap的数据结构是数组和链表的结合,我们当然希望这个HashMap里面的元素尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,当我们查询时不用遍历链表,这样就大大优化了查询效率。
对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用hash(int h)方法所得到的hash值总是相同的,我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样的:调用indexFor(int h,int length)方法来计算该对象应该保存在table数组的哪个索引处。
1 static int indexFor(int h, int length) { 2 return h & (length-1); 3 }
这个方法非常巧妙,它通过h&(table.length-1)来得到该对象的索引,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。在HashMap构造函数中有如下代码:
1 // Find a power of 2 >= initialCapacity 2 int capacity = 1; 3 while (capacity < initialCapacity) 4 capacity <<= 1;
这段代码保证初始化时HashMap的容量总是2的n次方,即底层的长度总是2的n次方。当length是2的n次方时,h&(length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
当数组长度是2的n次方时,2n-1得到的二进制数的每位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。所以说,当数组长度为2的n次幂的时候,不同的key计算得到的index相同的几率较小。那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率就较高了。
3.3取
根据key取对应的value,key可以是null,其索引是0
根据key取出其对应的Entry(键值对),key不存在时,返回null
HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据hash算法来决定其在数组中的存储位置,再根据equals方法决定其在该数组上的链表中的存储位置,当需要读取一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
3.4 Rehash
当哈希表中的元素数目达到阈值时,哈希表就自动进行Rehash,capacity加倍。值得注意的是,当capacity等于MAXIMUM_CAPACITY的时候,不会调整哈希表大小,但是会把阈值调为Integer.MAX_VALUE。rehash是一个比较耗时的操作。Rahash对应的函数是resize,如下
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); 11 table = newTable; 12 threshold = (int)(newCapacity * loadFactor); 13 }
resize调用transfer函数,它的作用是将久表中的元素重新映射到新表中。transfer函数的代码如下:
1 void transfer(Entry[] newTable) { 2 Entry[] src = table; 3 int newCapacity = newTable.length; 4 for (int j = 0; j < src.length; j++) { 5 Entry<K,V> e = src[j]; 6 if (e != null) { 7 src[j] = null; 8 do { 9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); 11 e.next = newTable[i]; 12 newTable[i] = e; 13 e = next; 14 } while (e != null); 15 } 16 } 17 }
3.5删除
根据key删除其对应的Entry是由函数remove实现的,remove函数调用removeEntryForKey实现,removeEntryForKey的代码如下:
1 final Entry<K,V> removeEntryForKey(Object key) { 2 int hash = (key == null) ? 0 : hash(key.hashCode()); 3 int i = indexFor(hash, table.length); 4 Entry<K,V> prev = table[i]; 5 Entry<K,V> e = prev; 6 7 while (e != null) { 8 Entry<K,V> next = e.next; 9 Object k; 10 if (e.hash == hash && 11 ((k = e.key) == key || (key != null && key.equals(k)))) { 12 modCount++; 13 size--; 14 if (prev == e) 15 table[i] = next; 16 else 17 prev.next = next; 18 e.recordRemoval(this); 19 return e; 20 } 21 prev = e; 22 e = next; 23 } 24 25 return e; 26 }
删除并返回key所对应的Entry,如果哈希表中不存在参数key对于的键值对,则返回null。
clear函数将哈希表置空,如下
1 /** 2 * Removes all of the mappings from this map. 3 * The map will be empty after this call returns. 4 */ 5 public void clear() { 6 modCount++; 7 Entry[] tab = table; 8 for (int i = 0; i < tab.length; i++) 9 tab[i] = null; 10 size = 0; 11 }
可以看出,函数遍历Entry数组,并将数组的每一项置为null。
3.6存在性
判断某个value是否存储在哈希表中,函数如下
1 /** 2 * Returns <tt>true</tt> if this map maps one or more keys to the 3 * specified value. 4 * 5 * @param value value whose presence in this map is to be tested 6 * @return <tt>true</tt> if this map maps one or more keys to the 7 * specified value 8 */ 9 public boolean containsValue(Object value) { 10 if (value == null) 11 return containsNullValue(); 12 13 Entry[] tab = table; 14 for (int i = 0; i < tab.length ; i++) 15 for (Entry e = tab[i] ; e != null ; e = e.next) 16 if (value.equals(e.value)) 17 return true; 18 return false; 19 }
没有巧妙的方法,就是遍历哈希表。value也可以取值为null,用专门的函数containsNullValue,代码如下:
1 /** 2 * Special-case code for containsValue with null argument 3 */ 4 private boolean containsNullValue() { 5 Entry[] tab = table; 6 for (int i = 0; i < tab.length ; i++) 7 for (Entry e = tab[i] ; e != null ; e = e.next) 8 if (e.value == null) 9 return true; 10 return false; 11 }
也是遍历哈希表。
4.Entry类
键值对存储在一个Entry中,HashMap的存储结构就是一个Entry数组,索引是数组的下标。数组的每一项对应一个桶,每个桶是一个Entry单向列表。代码如下
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; 5 final int hash; 6 7 /** 8 * Creates new entry. 9 */ 10 Entry(int h, K k, V v, Entry<K,V> n) { 11 value = v; 12 next = n; 13 key = k; 14 hash = h; 15 } 16 17 public final K getKey() { 18 return key; 19 } 20 21 public final V getValue() { 22 return value; 23 } 24 25 public final V setValue(V newValue) { 26 V oldValue = value; 27 value = newValue; 28 return oldValue; 29 } 30 31 public final boolean equals(Object o) { 32 if (!(o instanceof Map.Entry)) 33 return false; 34 Map.Entry e = (Map.Entry)o; 35 Object k1 = getKey(); 36 Object k2 = e.getKey(); 37 if (k1 == k2 || (k1 != null && k1.equals(k2))) { 38 Object v1 = getValue(); 39 Object v2 = e.getValue(); 40 if (v1 == v2 || (v1 != null && v1.equals(v2))) 41 return true; 42 } 43 return false; 44 } 45 46 public final int hashCode() { 47 return (key==null ? 0 : key.hashCode()) ^ 48 (value==null ? 0 : value.hashCode()); 49 } 50 51 public final String toString() { 52 return getKey() + "=" + getValue(); 53 } 54 55 /** 56 * This method is invoked whenever the value in an entry is 57 * overwritten by an invocation of put(k,v) for a key k that's already 58 * in the HashMap. 59 */ 60 void recordAccess(HashMap<K,V> m) { 61 } 62 63 /** 64 * This method is invoked whenever the entry is 65 * removed from the table. 66 */ 67 void recordRemoval(HashMap<K,V> m) { 68 } 69 }
参考
http://www.cnblogs.com/skywang12345/p/3310835.html
http://www.codeceo.com/article/java-hashmap-learn.html