浅析Java中HashMap的实现

概述

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 }
View Code

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;
View Code

如以上源码所示,定义了哈希表重要的参数。因为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     }
View Code

构造函数的重要功能是初始化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     }
View Code

这两个函数是求索引的,也就是键值对应该存放在哪个桶中。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     }
View Code

当我们向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     }
View Code

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         }
View Code

从代码中可以看到,向表中插入一个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     }
View Code

当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     }
View Code

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     }
View Code

这个方法非常巧妙,它通过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;
View Code

这段代码保证初始化时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取

View Code

根据key取对应的value,key可以是null,其索引是0

View Code

根据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     }
View Code

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     }
View Code

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     }
View Code

删除并返回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     }
View Code

可以看出,函数遍历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     }
View Code

没有巧妙的方法,就是遍历哈希表。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     }
View Code

也是遍历哈希表。

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     }
View Code

参考

http://www.cnblogs.com/skywang12345/p/3310835.html

http://www.codeceo.com/article/java-hashmap-learn.html

原文地址:https://www.cnblogs.com/qiaoshanzi/p/4813359.html