HashMap的实现原理



1.    HashMap概述:

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


2.    HashMap的数据结构:

   在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。


   从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

   源码如下:


Java代码  

 
   
 
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. }


   可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

 

3.    HashMap的存取实现:

   1) 存储:

Java代码   
   

 
   
 
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. 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值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

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


Java代码  

 
   
 
1. void addEntry(int hash, K key, V value, int bucketIndex) {  
2. // 获取指定 bucketIndex 索引处的 Entry   
3.     Entry<K,V> e = table[bucketIndex];  
4. // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
5. new Entry<K,V>(hash, key, value, e);  
6. // 如果 Map 中的 key-value 对的数量超过了极限  
7. if (size++ >= threshold)  
8. // 把 table 对象的长度扩充到原来的2倍。  
9. 2 * table.length);  
10. }


   当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

   hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。


Java代码  

 
   
 
1. static int hash(int h) {  
2. 20) ^ (h >>> 12);  
3. return h ^ (h >>> 7) ^ (h >>> 4);  
4. }


 

   我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的 元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:


Java代码  

 
   
 
1. static int indexFor(int h, int length) {  
2. return h & (length-1);  
3. }


 

   这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n次方,这是HashMap在速度上的优化。在 HashMap 构造器中有如下代码:


Java代码  

 
   
 
1. int capacity = 1;  
2. while (capacity < initialCapacity)  
3. 1;


HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。

当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

   这看上去很简单,其实比较有玄机的,我们举个例子来说明:

运算后的结果如下:

h & (table.length-1)                     hash                             table.length-1
8 & (15-1):                                 0100                   &              1110                   =                0100
9 & (15-1):                                 0101                   &              1110                   =                0100
-----------------------------------------------------------------------------------------------------------------------
       8 & (16-1):                                 0100                   &              1111                   =                0100
       9 & (16-1):                                 0101                   &              1111                   =                0101

   从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   

2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

   2) 读取:


Java代码  

 
   
 
1. public V get(Object key) {  
2. if (key == null)  
3. return getForNullKey();  
4. int hash = hash(key.hashCode());  
5. for (Entry<K,V> e = table[indexFor(hash, table.length)];  
6. null;  
7.         e = e.next) {  
8.         Object k;  
9. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
10. return e.value;  
11.     }  
12. return null;  
13. }


 

hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

  

HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

 

4.    HashMap的resize(rehash):

HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

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

 

5.    HashMap的性能参数:

HashMap 包含如下几个构造器:

HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。

initialCapacity:HashMap的最大容量,即为底层数组的长度。

loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

HashMap的实现中,通过threshold字段来判断HashMap的最大容量:


Java代码  

 
   
 
1. threshold = (int)(capacity * loadFactor);


threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

 


Java代码  

 
   
 
1. if (size++ >= threshold)     
2. 2 * table.length);



6.    Fail-Fast机制:

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

modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。


Java代码  

 
   
 
1. HashIterator() {  
2.     expectedModCount = modCount;  
3. if (size > 0) { // advance to first entry  
4.     Entry[] t = table;  
5. while (index < t.length && (next = t[index++]) == null)  
6.         ;  
7.     }  
8. }


 

modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:

modCount声明为volatile,保证线程之间修改的可见性。



Java代码  

 
   
 
1. final Entry<K,V> nextEntry() {     
2. if (modCount != expectedModCount)     
3. throw new ConcurrentModificationException();


 

   在HashMap的API中指出:

HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。




HashSet的实现原理



1.    HashSet概述:

HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。

 

2.    HashSet的实现:

HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, HashSet的源代码如下:



Java代码  

 
    
  
1. public class HashSet<E>  
2. extends AbstractSet<E>  
3. implements Set<E>, Cloneable, java.io.Serializable  
4. {  
5. static final long serialVersionUID = -5024744406713321676L;  
6.   
7. // 底层使用HashMap来保存HashSet中所有元素。  
8. private transient HashMap<E,Object> map;  
9.       
10. // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。  
11. private static final Object PRESENT = new Object();  
12.   
13. /**
14.      * 默认的无参构造器,构造一个空的HashSet。
15.      * 
16.      * 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
17.      */  
18. public HashSet() {  
19. new HashMap<E,Object>();  
20.     }  
21.   
22. /**
23.      * 构造一个包含指定collection中的元素的新set。
24.      *
25.      * 实际底层使用默认的加载因子0.75和足以包含指定
26.      * collection中所有元素的初始容量来创建一个HashMap。
27.      * @param c 其中的元素将存放在此set中的collection。
28.      */  
29. public HashSet(Collection<? extends E> c) {  
30. new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));  
31.     addAll(c);  
32.     }  
33.   
34. /**
35.      * 以指定的initialCapacity和loadFactor构造一个空的HashSet。
36.      *
37.      * 实际底层以相应的参数构造一个空的HashMap。
38.      * @param initialCapacity 初始容量。
39.      * @param loadFactor 加载因子。
40.      */  
41. public HashSet(int initialCapacity, float loadFactor) {  
42. new HashMap<E,Object>(initialCapacity, loadFactor);  
43.     }  
44.   
45. /**
46.      * 以指定的initialCapacity构造一个空的HashSet。
47.      *
48.      * 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
49.      * @param initialCapacity 初始容量。
50.      */  
51. public HashSet(int initialCapacity) {  
52. new HashMap<E,Object>(initialCapacity);  
53.     }  
54.   
55. /**
56.      * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
57.      * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
58.      *
59.      * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
60.      * @param initialCapacity 初始容量。
61.      * @param loadFactor 加载因子。
62.      * @param dummy 标记。
63.      */  
64. int initialCapacity, float loadFactor, boolean dummy) {  
65. new LinkedHashMap<E,Object>(initialCapacity, loadFactor);  
66.     }  
67.   
68. /**
69.      * 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。
70.      * 
71.      * 底层实际调用底层HashMap的keySet来返回所有的key。
72.      * 可见HashSet中的元素,只是存放在了底层HashMap的key上,
73.      * value使用一个static final的Object对象标识。
74.      * @return 对此set中元素进行迭代的Iterator。
75.      */  
76. public Iterator<E> iterator() {  
77. return map.keySet().iterator();  
78.     }  
79.   
80. /**
81.      * 返回此set中的元素的数量(set的容量)。
82.      *
83.      * 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。
84.      * @return 此set中的元素的数量(set的容量)。
85.      */  
86. public int size() {  
87. return map.size();  
88.     }  
89.   
90. /**
91.      * 如果此set不包含任何元素,则返回true。
92.      *
93.      * 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。
94.      * @return 如果此set不包含任何元素,则返回true。
95.      */  
96. public boolean isEmpty() {  
97. return map.isEmpty();  
98.     }  
99.   
100. /**
101.      * 如果此set包含指定元素,则返回true。
102.      * 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e))
103.      * 的e元素时,返回true。
104.      *
105.      * 底层实际调用HashMap的containsKey判断是否包含指定key。
106.      * @param o 在此set中的存在已得到测试的元素。
107.      * @return 如果此set包含指定元素,则返回true。
108.      */  
109. public boolean contains(Object o) {  
110. return map.containsKey(o);  
111.     }  
112.   
113. /**
114.      * 如果此set中尚未包含指定元素,则添加指定元素。
115.      * 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2))
116.      * 的元素e2,则向此set 添加指定的元素e。
117.      * 如果此set已包含该元素,则该调用不更改set并返回false。
118.      *
119.      * 底层实际将将该元素作为key放入HashMap。
120.      * 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key
121.      * 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),
122.      * 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变,
123.      * 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,
124.      * 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
125.      * @param e 将添加到此set中的元素。
126.      * @return 如果此set尚未包含指定元素,则返回true。
127.      */  
128. public boolean add(E e) {  
129. return map.put(e, PRESENT)==null;  
130.     }  
131.   
132. /**
133.      * 如果指定元素存在于此set中,则将其移除。
134.      * 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e,
135.      * 则将其移除。如果此set已包含该元素,则返回true
136.      * (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。
137.      *
138.      * 底层实际调用HashMap的remove方法删除指定Entry。
139.      * @param o 如果存在于此set中则需要将其移除的对象。
140.      * @return 如果set包含指定元素,则返回true。
141.      */  
142. public boolean remove(Object o) {  
143. return map.remove(o)==PRESENT;  
144.     }  
145.   
146. /**
147.      * 从此set中移除所有元素。此调用返回后,该set将为空。
148.      *
149.      * 底层实际调用HashMap的clear方法清空Entry中所有元素。
150.      */  
151. public void clear() {  
152.     map.clear();  
153.     }  
154.   
155. /**
156.      * 返回此HashSet实例的浅表副本:并没有复制这些元素本身。
157.      *
158.      * 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。
159.      */  
160. public Object clone() {  
161. try {  
162. super.clone();  
163.             newSet.map = (HashMap<E, Object>) map.clone();  
164. return newSet;  
165. catch (CloneNotSupportedException e) {  
166. throw new InternalError();  
167.         }  
168.     }  
169. }



 

 

3. 相关说明:

1) 相关HashMap的实现原理,请参考我的上一遍总结:深入Java集合学习系列:HashMap的实现原理

HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。





LinkedHashSet的实现原理



1.    LinkedHashSet概述:

LinkedHashSet是具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。

Set,而其中至少一个线程修改了该Set,则它必须保持外部同步。

 

2.    LinkedHashSet的实现:

LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。

LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。LinkedHashSet的源代码如下:


Java代码  

 
   
 
1. public class LinkedHashSet<E>  
2. extends HashSet<E>  
3. implements Set<E>, Cloneable, java.io.Serializable {  
4.   
5. private static final long serialVersionUID = -2851667679971038690L;  
6.   
7. /**
8.      * 构造一个带有指定初始容量和加载因子的新空链接哈希set。
9.      *
10.      * 底层会调用父类的构造方法,构造一个有指定初始容量和加载因子的LinkedHashMap实例。
11.      * @param initialCapacity 初始容量。
12.      * @param loadFactor 加载因子。
13.      */  
14. public LinkedHashSet(int initialCapacity, float loadFactor) {  
15. super(initialCapacity, loadFactor, true);  
16.     }  
17.   
18. /**
19.      * 构造一个带指定初始容量和默认加载因子0.75的新空链接哈希set。
20.      *
21.      * 底层会调用父类的构造方法,构造一个带指定初始容量和默认加载因子0.75的LinkedHashMap实例。
22.      * @param initialCapacity 初始容量。
23.      */  
24. public LinkedHashSet(int initialCapacity) {  
25. super(initialCapacity, .75f, true);  
26.     }  
27.   
28. /**
29.      * 构造一个带默认初始容量16和加载因子0.75的新空链接哈希set。
30.      *
31.      * 底层会调用父类的构造方法,构造一个带默认初始容量16和加载因子0.75的LinkedHashMap实例。
32.      */  
33. public LinkedHashSet() {  
34. super(16, .75f, true);  
35.     }  
36.   
37. /**
38.      * 构造一个与指定collection中的元素相同的新链接哈希set。
39.      * 
40.      * 底层会调用父类的构造方法,构造一个足以包含指定collection
41.      * 中所有元素的初始容量和加载因子为0.75的LinkedHashMap实例。
42.      * @param c 其中的元素将存放在此set中的collection。
43.      */  
44. public LinkedHashSet(Collection<? extends E> c) {  
45. super(Math.max(2*c.size(), 11), .75f, true);  
46.         addAll(c);  
47.     }  
48. }


HashSet中,专为LinkedHashSet提供的构造方法如下,该方法为包访问权限,并未对外公开。


Java代码  

 
   
 
1. /**
2.      * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
3.      * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
4.      *
5.      * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
6.      * @param initialCapacity 初始容量。
7.      * @param loadFactor 加载因子。
8.      * @param dummy 标记。
9.      */  
10. int initialCapacity, float loadFactor, boolean dummy) {  
11. new LinkedHashMap<E,Object>(initialCapacity, loadFactor);  
12.     }


LinkedHashSet通过继承HashSet,底层使用LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。

 



LinkedHashMap的实现原理


1. LinkedHashMap概述:

   LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
   LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
   注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

 

2. LinkedHashMap的实现:

   对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:

1) Entry元素:

   LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:


Java代码  

 
   
 
1. /**
2.  * 双向链表的表头元素。
3.  */  
4. private transient Entry<K,V> header;  
5.   
6. /**
7.  * LinkedHashMap的Entry元素。
8.  * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。
9.  */  
10. private static class Entry<K,V> extends HashMap.Entry<K,V> {  
11.     Entry<K,V> before, after;  
12.     ……  
13. }


 2) 初始化:

   通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。如:


Java代码  

 
   
 
1. public LinkedHashMap(int initialCapacity, float loadFactor) {  
2. super(initialCapacity, loadFactor);  
3. false;  
4. }


    HashMap中的相关构造方法:


Java代码  

 
   
 
1. public HashMap(int initialCapacity, float loadFactor) {  
2. if (initialCapacity < 0)  
3. throw new IllegalArgumentException("Illegal initial capacity: " +  
4.                                            initialCapacity);  
5. if (initialCapacity > MAXIMUM_CAPACITY)  
6.         initialCapacity = MAXIMUM_CAPACITY;  
7. if (loadFactor <= 0 || Float.isNaN(loadFactor))  
8. throw new IllegalArgumentException("Illegal load factor: " +  
9.                                            loadFactor);  
10.   
11. // Find a power of 2 >= initialCapacity  
12. int capacity = 1;  
13. while (capacity < initialCapacity)  
14. 1;  
15.   
16. this.loadFactor = loadFactor;  
17. int)(capacity * loadFactor);  
18. new Entry[capacity];  
19.     init();  
20. }


    我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器
中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。
   LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。


Java代码  

 
   
 
1. void init() {  
2. new Entry<K,V>(-1, null, null, null);  
3.     header.before = header.after = header;  
4. }


3) 存储:

   LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。


Java代码  

 
   
 
1. void addEntry(int hash, K key, V value, int bucketIndex) {  
2. // 调用create方法,将新元素以双向链表的的形式加入到映射中。  
3.     createEntry(hash, key, value, bucketIndex);  
4.   
5. // 删除最近最少使用元素的策略定义  
6.     Entry<K,V> eldest = header.after;  
7. if (removeEldestEntry(eldest)) {  
8.         removeEntryForKey(eldest.key);  
9. else {  
10. if (size >= threshold)  
11. 2 * table.length);  
12.     }  
13. }


Java代码  

 
   
 
1. void createEntry(int hash, K key, V value, int bucketIndex) {  
2.     HashMap.Entry<K,V> old = table[bucketIndex];  
3. new Entry<K,V>(hash, key, value, old);  
4.     table[bucketIndex] = e;  
5. // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。  
6.     e.addBefore(header);  
7.     size++;  
8. }


Java代码  

 
   
 
1. private void addBefore(Entry<K,V> existingEntry) {  
2.     after  = existingEntry;  
3.     before = existingEntry.before;  
4. this;  
5. this;  
6. }


  4) 读取:

   LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。


Java代码  

 
   
 
1. public V get(Object key) {  
2. // 调用父类HashMap的getEntry()方法,取得要查找的元素。  
3.     Entry<K,V> e = (Entry<K,V>)getEntry(key);  
4. if (e == null)  
5. return null;  
6. // 记录访问顺序。  
7. this);  
8. return e.value;  
9. }


Java代码  

 
   
 
1. void recordAccess(HashMap<K,V> m) {  
2.     LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
3. // 如果定义了LinkedHashMap的迭代顺序为访问顺序,  
4. // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。  
5. if (lm.accessOrder) {  
6.         lm.modCount++;  
7.         remove();  
8.         addBefore(lm.header);  
9.     }  
10. }


 5) 排序模式:

   LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。


Java代码  

 
   
 
1. private final boolean accessOrder;


 一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。看LinkedHashMap的构造方法,如:


Java代码  

 
   
 
1. public LinkedHashMap(int initialCapacity, float loadFactor) {  
2. super(initialCapacity, loadFactor);  
3. false;  
4. }


    这些构造方法都会默认指定排序模式为插入顺序。如果你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造LinkedHashMap:


Java代码  

 
   
 
1. public LinkedHashMap(int initialCapacity,  
2. float loadFactor,  
3. boolean accessOrder) {  
4. super(initialCapacity, loadFactor);  
5. this.accessOrder = accessOrder;  
6. }


    该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建LRU缓存。LinkedHashMap提供了removeEldestEntry(Map.Entry<K,V> eldest)方法,在将新条目插入到映射后,put和 putAll将调用此方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回false,这样,此映射的行为将类似于正常映射,即永远不能移除最旧的元素。


Java代码  

 
   
 
1. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {  
2. return false;  
3. }


    此方法通常不以任何方式修改映射,相反允许映射在其返回值的指引下进行自我修改。如果用此映射构建LRU缓存,则非常方便,它允许映射通过删除旧条目来减少内存损耗。
   例如:重写此方法,维持此映射只保存100个条目的稳定状态,在每次添加新条目时删除最旧的条目。


Java代码  

 
   
 
1. private static final int MAX_ENTRIES = 100;  
2. protected boolean removeEldestEntry(Map.Entry eldest) {  
3. return size() > MAX_ENTRIES;  
4. }

 


ArrayList的实现原理



1. ArrayList概述:

   ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。
   每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 
   注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

 

2. ArrayList的实现:

   对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:

1) 底层使用数组实现:


Java代码  

 
   
 
1. private transient Object[] elementData;


2) 构造方法: 
   ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。


Java代码  

 
   
 
1. public ArrayList() {  
2. this(10);  
3. }  
4.   
5. public ArrayList(int initialCapacity) {  
6. super();  
7. if (initialCapacity < 0)  
8. throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);  
9. this.elementData = new Object[initialCapacity];  
10. }  
11.   
12. public ArrayList(Collection<? extends E> c) {  
13.     elementData = c.toArray();  
14.     size = elementData.length;  
15. // c.toArray might (incorrectly) not return Object[] (see 6260652)  
16. if (elementData.getClass() != Object[].class)  
17. class);  
18. }


 3) 存储: 
   ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。下面我们一一讲解:


Java代码  

 
   
 
1. // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。  
2. public E set(int index, E element) {  
3.     RangeCheck(index);  
4.   
5.     E oldValue = (E) elementData[index];  
6.     elementData[index] = element;  
7. return oldValue;  
8. }


Java代码  

 
   
 
1. // 将指定的元素添加到此列表的尾部。  
2. public boolean add(E e) {  
3. 1);   
4.     elementData[size++] = e;  
5. return true;  
6. }


Java代码  

 
   
 
1. // 将指定的元素插入此列表中的指定位置。  
2. // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
3. public void add(int index, E element) {  
4. if (index > size || index < 0)  
5. throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
6. // 如果数组长度不足,将进行扩容。  
7. 1);  // Increments modCount!!  
8. // 将 elementData中从Index位置开始、长度为size-index的元素,  
9. // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
10. // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
11. 1, size - index);  
12.     elementData[index] = element;  
13.     size++;  
14. }


Java代码  

 
   
 
1. // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。  
2. public boolean addAll(Collection<? extends E> c) {  
3.     Object[] a = c.toArray();  
4. int numNew = a.length;  
5. // Increments modCount  
6. 0, elementData, size, numNew);  
7.     size += numNew;  
8. return numNew != 0;  
9. }


Java代码  

 
   
 
1. // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。  
2. public boolean addAll(int index, Collection<? extends E> c) {  
3. if (index > size || index < 0)  
4. throw new IndexOutOfBoundsException(  
5. "Index: " + index + ", Size: " + size);  
6.   
7.     Object[] a = c.toArray();  
8. int numNew = a.length;  
9. // Increments modCount  
10.   
11. int numMoved = size - index;  
12. if (numMoved > 0)  
13.         System.arraycopy(elementData, index, elementData, index + numNew, numMoved);  
14.   
15. 0, elementData, index, numNew);  
16.     size += numNew;  
17. return numNew != 0;  
18. }


 4) 读取:


Java代码  

 
   
 
1. // 返回此列表中指定位置上的元素。  
2. public E get(int index) {  
3.     RangeCheck(index);  
4.   
5. return (E) elementData[index];  
6. }


  5) 删除: 
   ArrayList提供了根据下标或者指定对象两种方式的删除功能。如下:


Java代码  

 
   
 
1. // 移除此列表中指定位置上的元素。  
2. public E remove(int index) {  
3.     RangeCheck(index);  
4.   
5.     modCount++;  
6.     E oldValue = (E) elementData[index];  
7.   
8. int numMoved = size - index - 1;  
9. if (numMoved > 0)  
10. 1, elementData, index, numMoved);  
11. null; // Let gc do its work  
12.   
13. return oldValue;  
14. }


Java代码  

 
   
 
1. // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。  
2. public boolean remove(Object o) {  
3. // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。  
4. if (o == null) {  
5. for (int index = 0; index < size; index++)  
6. if (elementData[index] == null) {  
7. // 类似remove(int index),移除列表中指定位置上的元素。  
8.                 fastRemove(index);  
9. return true;  
10.             }  
11. } else {  
12. for (int index = 0; index < size; index++)  
13. if (o.equals(elementData[index])) {  
14.             fastRemove(index);  
15. return true;  
16.         }  
17.     }  
18. return false;  
19. }


    注意:从数组中移除元素的操作,也会导致被移除的元素以后的所有元素的向左移动一个位置。
   6) 调整数组容量: 
   从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。


Java代码  

HashMap,HashSet,LinkedHashSet,LinkedHashMap,ArrayList实现原理_链表

 
   
 
1. public void ensureCapacity(int minCapacity) {  
2.     modCount++;  
3. int oldCapacity = elementData.length;  
4. if (minCapacity > oldCapacity) {  
5.         Object oldData[] = elementData;  
6. int newCapacity = (oldCapacity * 3)/2 + 1;  
7. if (newCapacity < minCapacity)  
8.                 newCapacity = minCapacity;  
9. // minCapacity is usually close to size, so this is a win:  
10.       elementData = Arrays.copyOf(elementData, newCapacity);  
11.     }  
12. }

   从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
   ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:


Java代码  

 
   
 
1. public void trimToSize() {  
2.     modCount++;  
3. int oldCapacity = elementData.length;  
4. if (size < oldCapacity) {  
5.         elementData = Arrays.copyOf(elementData, size);  
6.     }  
7. }


7) Fail-Fast机制: 
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考我之前的文章深入Java集合学习系列:HashMap的实现原理 中的Fail-Fast机制。
   8) 关于其他