本篇文章是網上多篇文章的精華的總結,結合自己看源代碼的一些感悟,其中線程安全性和性能測試部分並未做實踐測試,直接是“拿來”網上的博客的。
哈希表概述
哈希表本質上一個數組,數組中每一個元素稱為一個箱子(Bin),箱子中存放的是鍵值對Entry<K,V>鏈表,因而也稱之為鏈表散列。
我們可以用圖來形象地說明這個結構:
哈希表是如何工作的?
存儲
Step1:根據哈希函數來計算HashCode值h,其中鍵值對Entry<K,V>的K來計算時需要的參數。
Step2:根據HashCode,來計算存放在哈希表(長度為n)中的位置(箱子的位置),一種計算方法是取余:h%n。
Step3:如果該箱子中已經存在鍵值對數據,則使用開放尋址法或拉鏈法解決沖突。
獲取
Step1:根據key值計算HashCode的值h。
Step2:假設箱子的個數為 n,那么這個鍵值對應該放在第 (h % n) 個箱子中。
Step3:如果這個箱子里有多個鍵值對,同時假設箱子里的多個值是采用鏈表的方式存儲,則需要遍歷這個鏈表,復雜度為O(n)。
擴容
哈希表還有 一個重要的屬性:負載因子,它是衡量哈希表的空/滿程度,一定程度上也能體現查詢的效率。其計算公式為:
負載因子 = 總鍵值對數 / 箱子數量
負載因子越大,意味着哈希表越滿,越容易導致沖突(更大的概念找到同一個箱子上),因而查詢效率也就更低。因而,一般來說,當負載因子大於某個常數(可能是1,也可能是其他值,Java8的HashMap的負載因子為0.75)時,哈希表就會自動擴容。
哈希表在擴容的時候,一般都會選擇擴大2的倍數,同時將原來的哈希表的數據遷移到新的哈希表中,這樣即使key的哈希值不變,對箱子的取余結果(假設我們用這種方法來計算HashCode)也會不同,因此所有的箱子和元素的存放位置都有可能發生變化,這個過程也稱為重哈希(rehash)。
哈表的擴容並不能有效解決負載因子過大的問題,因為在前面的取HashCode的方法中,假設所有key的HashCode值都一樣,那么即使擴容以后他們在哈希表中的位置也不會變,實際存放在箱子中的鏈表長度也不變,因此也就不能提高哈希表的查詢速度。
因而,哈希表存在以下兩個問題:
1、在擴容的時候,重哈希的成本比較大
2、如果Hash函數設計地不合理(如上面舉例說明的取余),會導致哈希表中極端情況下變成線性表,性能極低。
我們下面來看看Java8中是如何處理這兩個問題的。
以上這部分內容多參考自:深入理解哈希表 ,圖片來自於HashMap的圖示
Java8中的HashMap
在說明這個問題之前,我們來看下HashMap在Java8中在類圖關系,如下所示:
Java8中通過如下幾種方式來解決上面的兩個問題:
一、讓元素分布地更合理
(下面這部分不知道是哪位大神寫的,原文照抄吧)
學過概率論的讀者也許知道,理想狀態下哈希表的每個箱子中,元素的數量遵守泊松分布:
當負載因子為 0.75 時,上述公式中 λ 約等於 0.5,因此箱子中元素個數和概率的關系如下:
數量 | 概率 |
---|---|
0 | 0.60653066 |
1 | 0.30326533 |
2 | 0.07581633 |
3 | 0.01263606 |
4 | 0.00157952 |
5 | 0.00015795 |
6 | 0.00001316 |
7 | 0.00000094 |
8 | 0.00000006 |
這就是為什么我們將0.75設為負載因子,同時針對箱子中鏈表長度超過8以后要做另外的優化(一來是優化的概念較小,二來是優化過后的效率提升明顯)。所以,一般情況下負載因子不建議修改;同時如果在數量為8的鏈表的概率較大,則幾乎可以認為是哈希函數設計有問題導致的。
二、通過紅黑樹讓查詢更有效率(O(n)—>O(Log(n)))
第一點已經說明,當箱子中的鏈表元素超過8個時,會將這個鏈表轉為紅黑樹,紅黑樹的查找效率為O(log(n))。紅黑樹的示圖如下:
三、讓擴容時重哈希(rehash)的成本變得更小
在Java7中,重哈希是要重新計算Hash值的,而在Java8中,通過高位運算的巧妙設計,避免了這種計算。下面我們舉例說明:
我們要在初始大小為2的HashMap中存儲3、5、7這3個值,Hash函數為取余法。
Step1:在開始的時候,3、5、7經過Hash過后 3%2=1、5%2=1、7%2=1,因而3、5、7存儲在同一個箱子的鏈表中(地址為1)。
Step2:現在擴容了,擴容后的大小為2*2=4,現在經過Hash后3%4=3、5%4=1、7%4=3,因而3與7一起放在箱子的鏈表中(地址為3),5單獨存放在一個箱子里(地址為1)。
整個過程如下圖所示:
我們注意到,在擴容后3和7的位置變化了,由1—>3(=1+2)
再進行擴容,由4容為8,那么經過Hash后,3%8=3、5%8=5、7%8=7,分別存放於3、5(=1+4)、7(=3+4)這幾個位置中。
我們發現,擴容后的元素要么在原位置,要么在原位置再移動2次冪的位置,整個過程只需要使用一個位運算符<<就可以了(在源碼的resize方法中可以找到)。
我們用計算機的地址來展示這個過程:
n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:
這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。
以上這部分中的圖示和位移講解的內容參考自:深入分析hashmap
另外:
四:我們可以通過適當地初始化大小來控制擴容的次數:既然擴容是不可避免的,我們就盡可能少地讓它發生,要實際編程的時候,應該根據業務合理地設置初始大小的值。
此外,Java8中HashMap還提供了另外一些參數來控制HashMap的性能,如下所示:
/** * 默認的初始化大小(必須為2的冪) */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * 最大的存儲數量(默認的數量,可以在構造函數中指定) * 必須為2的冪同時小於2的30次方 */
static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默認的負載因子,可以在構建函數中指定 */
static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * HashMap由鏈表轉為紅黑樹存儲的閥值 * 1.8提供的新特性 */
static final int TREEIFY_THRESHOLD = 8; /** * HashMap由紅黑樹轉為鏈表存儲的閥值 */
static final int UNTREEIFY_THRESHOLD = 6; /** * HashMap的箱子中的鏈表轉為紅黑樹之前還有一個判斷: * 只在所有箱子(鍵值對)的數量大於64才會發生轉換 * 這樣是為了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表而導致不必要的轉化 */
static final int MIN_TREEIFY_CAPACITY = 64;
源碼中的關鍵方法
方法一、hash方法
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//這里其實就要求大家來重寫HashCode方法 4 }
方法二、putVal方法
下面是putVal方法的執行過程圖示:

1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 // 步驟①:tab為空則創建
5 if ((tab = table) == null || (n = tab.length) == 0) 6 n = (tab = resize()).length; 7 // 步驟②:計算index,並對null做處理
8 if ((p = tab[i = (n - 1) & hash]) == null) 9 tab[i] = newNode(hash, key, value, null); 10 else { 11 Node<K,V> e; K k; 12 // 步驟③:節點key存在,直接覆蓋value
13 if (p.hash == hash &&
14 ((k = p.key) == key || (key != null && key.equals(k)))) 15 e = p; 16 // 步驟④:判斷該鏈為紅黑樹
17 else if (p instanceof TreeNode) 18 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 19 // 步驟⑤:該鏈為鏈表
20 else { 21 for (int binCount = 0; ; ++binCount) { 22 if ((e = p.next) == null) { 23 p.next = newNode(hash, key,value,null); 24 //鏈表長度大於8轉換為紅黑樹進行處理
25 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
26 treeifyBin(tab, hash); 27 break; 28 } 29 // key已經存在直接覆蓋value
30 if (e.hash == hash &&
31 ((k = e.key) == key || (key != null && key.equals(k)))) 32 break; 33 p = e; 34 } 35 } 36
37 if (e != null) { // existing mapping for key
38 V oldValue = e.value; 39 if (!onlyIfAbsent || oldValue == null) 40 e.value = value; 41 afterNodeAccess(e); 42 return oldValue; 43 } 44 } 45 ++modCount; 46 // 步驟⑥:超過最大容量 就擴容
47 if (++size > threshold) 48 resize(); 49 afterNodeInsertion(evict); 50 return null; 51 }
這個 getNode() 方法就是根據哈希表元素個數與哈希值求模(使用的公式是 (n - 1) &hash
)得到 key 所在的桶的頭結點,如果頭節點恰好是紅黑樹節點,就調用紅黑樹節點的 getTreeNode() 方法,否則就遍歷鏈表節點。
方法三、節點查找方法getNode

1 final Node<K,V> getNode(int hash, Object key) { 2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 3 if ((tab = table) != null && (n = tab.length) > 0 &&
4 (first = tab[(n - 1) & hash]) != null) { 5 if (first.hash == hash && // always check first node
6 ((k = first.key) == key || (key != null && key.equals(k)))) 7 return first; 8 if ((e = first.next) != null) { 9 if (first instanceof TreeNode) 10 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 11 do { 12 if (e.hash == hash &&
13 ((k = e.key) == key || (key != null && key.equals(k)))) 14 return e; 15 } while ((e = e.next) != null); 16 } 17 } 18 return null; 19 }
方法四、紅黑樹生成方法

1 //將桶內所有的 鏈表節點 替換成 紅黑樹節點
2
3 final void treeifyBin(Node<K,V>[] tab, int hash) { 4
5 int n, index; Node<K,V> e; 6
7 //如果當前哈希表為空,或者哈希表中元素的個數小於 進行樹形化的閾值(默認為 64),就去新建/擴容
8
9 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 10
11 resize(); 12
13 else if ((e = tab[index = (n - 1) & hash]) != null) { 14
15 //如果哈希表中的元素個數超過了 樹形化閾值,進行樹形化 16
17 // e 是哈希表中指定位置桶里的鏈表節點,從第一個開始
18
19 TreeNode<K,V> hd = null, tl = null; //紅黑樹的頭、尾節點
20
21 do { 22
23 //新建一個樹形節點,內容和當前鏈表節點 e 一致
24
25 TreeNode<K,V> p = replacementTreeNode(e, null); 26
27 if (tl == null) //確定樹頭節點
28
29 hd = p; 30
31 else { 32
33 p.prev = tl; 34
35 tl.next = p; 36
37 } 38
39 tl = p; 40
41 } while ((e = e.next) != null); 42
43 //讓桶的第一個元素指向新建的紅黑樹頭結點,以后這個桶里的元素就是紅黑樹而不是鏈表了
44
45 if ((tab[index] = hd) != null) 46
47 hd.treeify(tab); 48
49 } 50
51 } 52
53 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { 54
55 return new TreeNode<>(p.hash, p.key, p.value, next); 56
57 }
方法五、紅黑樹節點查找方法

1 final TreeNode<K,V> find(int h, Object k, Class<?> kc) { 2 TreeNode<K,V> p = this; 3 do { 4 int ph, dir; K pk; 5 TreeNode<K,V> pl = p.left, pr = p.right, q; 6 if ((ph = p.hash) > h) 7 p = pl; 8 else if (ph < h) 9 p = pr; 10 else if ((pk = p.key) == k || (k != null && k.equals(pk))) 11 return p; 12 else if (pl == null) 13 p = pr; 14 else if (pr == null) 15 p = pl; 16 else if ((kc != null ||
17 (kc = comparableClassFor(k)) != null) &&
18 (dir = compareComparables(kc, k, pk)) != 0) 19 p = (dir < 0) ? pl : pr; 20 else if ((q = pr.find(h, k, kc)) != null) 21 return q; 22 else
23 p = pl; 24 } while (p != null); 25 return null; 26 }
方法六、擴容方法

1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) { 7 // 超過最大值就不再擴充了,就只好隨你碰撞去吧
8 if (oldCap >= MAXIMUM_CAPACITY) { 9 threshold = Integer.MAX_VALUE; 10 return oldTab; 11 } 12 // 沒超過最大值,就擴充為原來的2倍
13 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
14 oldCap >= DEFAULT_INITIAL_CAPACITY) 15 newThr = oldThr << 1; // double threshold
16 } 17 else if (oldThr > 0) // initial capacity was placed in threshold
18 newCap = oldThr; 19 else { // zero initial threshold signifies using defaults
20 newCap = DEFAULT_INITIAL_CAPACITY; 21 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 22 } 23 // 計算新的resize上限
24 if (newThr == 0) { 25
26 float ft = (float)newCap * loadFactor; 27 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
28 (int)ft : Integer.MAX_VALUE); 29 } 30 threshold = newThr; 31 @SuppressWarnings({"rawtypes","unchecked"}) 32 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 33 table = newTab; 34 if (oldTab != null) { 35 // 把每個bucket都移動到新的buckets中
36 for (int j = 0; j < oldCap; ++j) { 37 Node<K,V> e; 38 if ((e = oldTab[j]) != null) { 39 oldTab[j] = null; 40 if (e.next == null) 41 newTab[e.hash & (newCap - 1)] = e; 42 else if (e instanceof TreeNode) 43 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 44 else { // preserve order
45 Node<K,V> loHead = null, loTail = null; 46 Node<K,V> hiHead = null, hiTail = null; 47 Node<K,V> next; 48 do { 49 next = e.next; 50 // 原索引
51 if ((e.hash & oldCap) == 0) { 52 if (loTail == null) 53 loHead = e; 54 else
55 loTail.next = e; 56 loTail = e; 57 } 58 // 原索引+oldCap
59 else { 60 if (hiTail == null) 61 hiHead = e; 62 else
63 hiTail.next = e; 64 hiTail = e; 65 } 66 } while ((e = next) != null); 67 // 原索引放到bucket里
68 if (loTail != null) { 69 loTail.next = null; 70 newTab[j] = loHead; 71 } 72 // 原索引+oldCap放到bucket里
73 if (hiTail != null) { 74 hiTail.next = null; 75 newTab[j + oldCap] = hiHead; 76 } 77 } 78 } 79 } 80 } 81 return newTab; 82 }
方法七、擴容后的元素轉移方法

1 void transfer(Entry[] newTable) { 2 Entry[] src = table; //src引用了舊的Entry數組
3 int newCapacity = newTable.length; 4 for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組
5 Entry<K,V> e = src[j]; //取得舊Entry數組的每個元素
6 if (e != null) { 7 src[j] = null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)
8 do { 9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置
11 e.next = newTable[i]; //標記[1]
12 newTable[i] = e; //將元素放在數組上
13 e = next; //訪問下一個Entry鏈上的元素
14 } while (e != null); 15 } 16 } 17 17 }
線程安全性
在多線程使用場景中,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap。那么為什么說HashMap是線程不安全的,下面舉例子說明在並發的多線程使用場景中使用HashMap可能造成死循環。代碼例子如下(便於理解,仍然使用JDK1.7的環境):
1 public class HashMapInfiniteLoop { 2
3 private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f); 4 public static void main(String[] args) { 5 map.put(5, "C"); 6
7 new Thread("Thread1") { 8 public void run() { 9 map.put(7, "B"); 10 System.out.println(map); 11 }; 12 }.start(); 13 new Thread("Thread2") { 14 public void run() { 15 map.put(3, "A);
16 System.out.println(map); 17 }; 18 }.start(); 19 } 20 }
其中,map初始化為一個長度為2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就需要進行resize。
通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節代碼塊)的首行。注意此時兩個線程已經成功添加數據。放開thread1的斷點至transfer方法的“Entry next = e.next;” 這一行;然后放開線程2的的斷點,讓線程2進行resize。結果如下圖:
注意,Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。
線程一被調度回來執行,先是執行 newTalbe[i] = e, 然后是e = next,導致了e指向了key(7),而下一次循環的next = e.next導致了next指向了key(3)。
e.next = newTable[i] 導致 key(3).next 指向了 key(7)。注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。
於是,當我們用線程一調用map.get(11)時,悲劇就出現了——Infinite Loop。
性能表現:JDK1.8 vs JDK1.7
HashMap中,如果key經過hash算法得出的數組索引位置全部不相同,即Hash算法非常好,那樣的話,getKey方法的時間復雜度就是O(1),如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中,或者在一個紅黑樹中,時間復雜度分別為O(n)和O(lgn)。 鑒於JDK1.8做了多方面的優化,總體性能優於JDK1.7,下面我們從兩個方面用例子證明這一點。
Hash較均勻的情況
為了便於測試,我們先寫一個類Key,如下:
1 class Key implements Comparable<Key> { 2
3 private final int value; 4
5 Key(int value) { 6 this.value = value; 7 } 8
9 @Override 10 public int compareTo(Key o) { 11 return Integer.compare(this.value, o.value); 12 } 13
14 @Override 15 public boolean equals(Object o) { 16 if (this == o) return true; 17 if (o == null || getClass() != o.getClass()) 18 return false; 19 Key key = (Key) o; 20 return value == key.value; 21 } 22
23 @Override 24 public int hashCode() { 25 return value; 26 } 27 }
這個類復寫了equals方法,並且提供了相當好的hashCode函數,任何一個值的hashCode都不會相同,因為直接使用value當做hashcode。為了避免頻繁的GC,我將不變的Key實例緩存了起來,而不是一遍一遍的創建它們。代碼如下:
1 public class Keys { 2
3 public static final int MAX_KEY = 10_000_000; 4 private static final Key[] KEYS_CACHE = new Key[MAX_KEY]; 5
6 static { 7 for (int i = 0; i < MAX_KEY; ++i) { 8 KEYS_CACHE[i] = new Key(i); 9 } 10 } 11
12 public static Key of(int value) { 13 return KEYS_CACHE[value]; 14 } 15 }
現在開始我們的試驗,測試需要做的僅僅是,創建不同size的HashMap(1、10、100、……10000000),屏蔽了擴容的情況,代碼如下:
1 static void test(int mapSize) { 2
3 HashMap<Key, Integer> map = new HashMap<Key,Integer>(mapSize); 4 for (int i = 0; i < mapSize; ++i) { 5 map.put(Keys.of(i), i); 6 } 7
8 long beginTime = System.nanoTime(); //獲取納秒
9 for (int i = 0; i < mapSize; i++) { 10 map.get(Keys.of(i)); 11 } 12 long endTime = System.nanoTime(); 13 System.out.println(endTime - beginTime); 14 } 15
16 public static void main(String[] args) { 17 for(int i=10;i<= 1000 0000;i*= 10){ 18 test(i); 19 } 20 }
在測試中會查找不同的值,然后度量花費的時間,為了計算getKey的平均時間,我們遍歷所有的get方法,計算總的時間,除以key的數量,計算一個平均值,主要用來比較,絕對值可能會受很多環境因素的影響。結果如下:
通過觀測測試結果可知,JDK1.8的性能要高於JDK1.7 15%以上,在某些size的區域上,甚至高於100%。由於Hash算法較均勻,JDK1.8引入的紅黑樹效果不明顯,下面我們看看Hash不均勻的的情況。
Hash極不均勻的情況
假設我們又一個非常差的Key,它們所有的實例都返回相同的hashCode值。這是使用HashMap最壞的情況。代碼修改如下:
1 class Key implements Comparable<Key> { 2
3 //...
4
5 @Override 6 public int hashCode() { 7 return 1; 8 } 9 }
仍然執行main方法,得出的結果如下表所示:
從表中結果中可知,隨着size的變大,JDK1.7的花費時間是增長的趨勢,而JDK1.8是明顯的降低趨勢,並且呈現對數增長穩定。當一個鏈表太長的時候,HashMap會動態的將它替換成一個紅黑樹,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同,這兩種情況的相對比較,可以說明一個好的hash算法的重要性。
小結
1.有兩個字典,分別存有 100 條數據和 10000 條數據,如果用一個不存在的 key 去查找數據,在哪個字典中速度更快?
完整的答案是:在 Redis 中,得益於自動擴容和默認哈希函數,兩者查找速度一樣快。在 Java 和 Objective-C 中,如果哈希函數不合理,返回值過於集中,會導致大字典更慢。Java 由於存在鏈表和紅黑樹互換機制,搜索時間呈對數級增長,而非線性增長。在理想的哈希函數下,無論字典多大,搜索速度都是一樣快。
2. 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
3. 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。
4. HashMap是線程不安全的,不要在並發的環境中同時操作HashMap,建議使用ConcurrentHashMap。
5. JDK1.8引入紅黑樹大程度優化了HashMap的性能。
參考文檔
https://tech.meituan.com/java-hashmap.html
https://www.cnblogs.com/gotodsp/p/6534699.html
https://www.cnblogs.com/shengkejava/p/6771469.html
http://www.importnew.com/14417.html 這篇講性能的文章值得一看
http://alex09.iteye.com/blog/539545 這是講Java7中的HashMap的
https://www.cnblogs.com/chinajava/p/5808416.html 這里面講了Java的HashMap和Redis的HashMap對比
http://blog.csdn.net/Richard_Jason/article/details/53887222
http://www.importnew.com/7099.html