天天看點

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

對于HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序,發現網上有很多人都了解錯了。

比如,有的人認為:

  Hashtable.keySet()          降序

  TreeMap.keySet()            升序

  HashMap.keySet()            亂序

  LinkedHashMap.keySet()      原序

也有的人認為是keyset跟entryset的差別導緻的。那麼下面我就通過兩種周遊方式來給大家試驗一下。

把全部要論證的問題先抛在這裡,待會看代碼看結果:

1.HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

2.分别用 keyset跟entryset周遊出來的結果有差別嗎

3.HashMap、Hashtable、TreeMap、LinkedHashMap初始預設的大小

研究了一些天,有了些結果,都是通過代碼測試出來的,親測有效~~~   (分别對int,String進行測試,主要看key的排序)

(終極聲明,大家先不要吐槽範型的問題,我隻是做個測試,不影響實際操作,謝過)

先來重制一下網上的輿論結果,看我的代碼

package testCollections;

import java.util.*;
import java.util.Map.Entry;

public class TestOfOrderOfMap {

	//測試Keyset的排序
	public void testOfKeyset(Map map, Object obj[]){
		for(int i=0; i<obj.length; i++){
			map.put(obj[i], obj[i]);
		}
		
		if(map instanceof LinkedHashMap){//LinkedHashMap也是一種HashMap,是以對其測試要放在前面
			System.out.println("LinkedHashMap" +": " + map);
		}else if(map instanceof HashMap){
			System.out.println("HashMap" +": " + map);
		}else if(map instanceof Hashtable){
			System.out.println("Hashtable" +": " + map);
		}else if(map instanceof TreeMap){
			System.out.println("TreeMap" +": " + map);
		}
		
		System.out.print("key: ");
		Iterator it =  map.keySet().iterator();
    	 <span style="white-space:pre">	</span>while (it.hasNext()) {
        	<span style="white-space:pre">	</span>Object key = it.next();
                        System.out.print(key+" ");
       <span style="white-space:pre">		</span>}		
         <span style="white-space:pre">	</span>System.out.println();
	}
	
	//測試Entryset的排序
	public void testOfEntryset(Map map, Object obj[]){
		for(int i=0; i<obj.length; i++){
			map.put(obj[i], obj[i]);
		}

		if(map instanceof LinkedHashMap){//LinkedHashMap也是一種HashMap,是以對其測試要放在前面
			System.out.println("LinkedHashMap" +": " + map);
		}else if(map instanceof HashMap){
			System.out.println("HashMap" +": " + map);
		}else if(map instanceof Hashtable){
			System.out.println("Hashtable" +": " + map);
		}else if(map instanceof TreeMap){
			System.out.println("TreeMap" +": " + map);
		}
		
		System.out.print("key: ");
		Iterator it =  map.entrySet().iterator();
        <span style="white-space:pre">	</span>while (it.hasNext()) {
        	 <span style="white-space:pre">	</span>Entry e = (Entry) it.next();
           <span style="white-space:pre">		</span>System.out.print(e.getKey()+" ");
        <span style="white-space:pre">	</span>}
       <span style="white-space:pre">	</span>       System.out.println();
	}
	
	 public static void main(String[] args) {
		 TestOfOrderOfMap test = new TestOfOrderOfMap();
		 
		 HashMap hm1 = new HashMap();					//第一組集合是用來測試int類型的key
		 Hashtable ht1 = new Hashtable();	 
		 TreeMap tm1 = new TreeMap();
		 LinkedHashMap lhm1 = new LinkedHashMap();		
		 
		 Integer a[] = new Integer[]{3, 2, 5, 1, 4};
		 
		 //測試Keyset中key為數字的排序
		 System.out.println("這裡是KeySet的測試:");
		 test.testOfKeyset(hm1, a);
		 test.testOfKeyset(ht1, a);
		 test.testOfKeyset(tm1, a);
		 test.testOfKeyset(lhm1, a);
		 
		//測試Entryset中key為數字的排序
		 System.out.println();
		 System.out.println("這裡是EntrySet的測試:");
		 test.testOfEntryset(hm1, a);
		 test.testOfEntryset(ht1, a);
		 test.testOfEntryset(tm1, a);
		 test.testOfEntryset(lhm1, a);
		 
	 }
}
           

看結果

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖1

大家是不是覺得就跟上面說的一樣啊,部落客,你逗我們的吧?

  Hashtable.keySet()          降序

  TreeMap.keySet()            升序

  HashMap.keySet()            亂序

  LinkedHashMap.keySet()      原序

且聽部落客慢慢講解噢,在這裡可以肯定的是keyset 跟 entryset所周遊的結果完全是一樣的,證明了第二點,那麼第一點呢,

如果我把上面的 Integer a[] = new Integer[]{3, 2, 5, 1, 4}; 改為  Integer a[] = new Integer[]{13, 2, 5, 1, 4}; 也就是把3改為了13,

你們再看結果

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖2

是不是想說,咦,這是什麼鬼。是不是上面的論斷結果是錯誤的了呀,

  Hashtable.keySet()          降序    //錯誤

  TreeMap.keySet()            升序    //正确

  HashMap.keySet()            亂序    //可以說錯誤,因為你還不懂它的原理

  LinkedHashMap.keySet()      原序  //可以說錯誤,因為你還不懂它的原理,這和LinkedHashMap的構造方法有關,後面我會說

再來看幾組結果,聽我分析。(以後我隻列出keyset的結果截圖,因為entryset 跟 keyset一樣)

如果我把上面的 Integer a[] = new Integer[]{3, 2, 5, 1, 4}; 改為  Integer a[] = new Integer[]{16, 2, 5, 1, 4};

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖3

如果我把上面的 Integer a[] = new Integer[]{3, 2, 5, 1, 4}; 改為  Integer a[] = new Integer[]{11, 2, 5, 1, 4};

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖4

如果我把上面的 Integer a[] = new Integer[]{3, 2, 5, 1, 4}; 改為  Integer a[] = new Integer[]{0, 65, 3, 17, 32};

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖5

大家看出端倪沒有,是這樣的,如果看過這些集合類源碼的哥哥姐姐可能就會知道,它們的周遊與它們的存儲有關,而存儲與容器的大小有關,

而在我們建立容器卻沒有指定容器大小的情況下,容器一般都會有一個預設大小,這個很好了解。

HashMap 的預設大小為16,每次增長會加倍,Hashtable的預設大小為11,每次增長原來的2倍加1,TreeMap 跟 LinkedHashMap暫時無法确定或者說無限大,

不過在這裡不影響我們的了解。好了,HashMap是會根據key的hashCode再進行hash計算,最後散列到Entry中,看源碼

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);<span style="white-space:pre">		</span>//求key的hash值
        int i = indexFor(hash, table.length); //找準位置插下去
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);//将其添加到該哈希值對應的連結清單中
        return null;
    }
           
final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);<span style="white-space:pre">	</span>//這裡很複雜,重新計算了哈希值
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
           
static int indexFor(int h, int length) {
        return h & (length-1);      //實際上就是求模
    }
           
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);    //擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);// 将“key-value”插入指定位置,bucketIndex是位置索引。
        }

        createEntry(hash, key, value, bucketIndex);
    }
           
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);// 建立Entry。将“key-value”插入指定位置。 
        size++;
    }
           

取value的過程也一樣,就是根據key存放的位置來取,是以說HashMap不是無序的,而是按照hash值來存取的,隻不過這個hash值,我們比較難以計算出來罷了。

有個小發現,當key的值不超過16時,HashMap的存取是按照從小到大的順序來存取的

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

       圖6

至于Hashtable的存取,跟HashMap大同小異,隻不過在方法前加了個synchronized同步字段,其次是hash值的計算有點小差別

有個小發現,當key的個數不超過11且key的值不超過11時,Hashtable的存取是按照從大到小的順序來存取的

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖7

T

reeMap就比較好了解了,其底層實作采用的是紅黑樹,key按照從小到大的順序排好,搞定。

LinkedHashMap這裡呢,有兩個順序,一個是插入的順序,一個是通路的順序,取決于構造方法裡面的參數accessOrder是否為true,當accessOrder為true時,周遊LinkedHashMap是按照通路順序存儲,當accessOrder為false時,LinkedHashMap是按照插入順序存儲,預設為false。看例子。

package testCollections;

import java.util.*;
import java.util.Map.Entry;

public class TestOfOrderOfMap {

	public void testOfKeyset(Map map, Object obj[]){
		for(int i=0; i<obj.length; i++){
			map.put(obj[i], obj[i]);
		}
		
		System.out.println("LinkedHashMap" +": " + map);
		System.out.print("key: ");
		Iterator it =  map.keySet().iterator();
    		while (it.hasNext()) {
        		Object key = it.next();
           	 System.out.print(key+" ");
       		}		
        	System.out.println();
	}
	
	 public static void main(String[] args) {
		 TestOfOrderOfMap test = new TestOfOrderOfMap();
		 LinkedHashMap lhm1 = new LinkedHashMap();		
		 Integer a[] = new Integer[]{7, 0, 5, 10, 17, 6, 2, 8, 3, 31};
		
		 test.testOfKeyset(lhm1, a);
	 }
}
           

結果:

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖8

插入的順序為Integer a[] = new Integer[]{7, 0, 5, 10, 17, 6, 2, 8, 3, 31}
           
列印的順序也是 7, 0, 5, 10, 17, 6, 2, 8, 3, 31
           

下面看按通路順序的,把上面的

LinkedHashMap lhm1 = new LinkedHashMap();	
           

改為  LinkedHashMap lhm1 = new LinkedHashMap(16, 0.75f,true);全部修改如下:

package testCollections;


import java.util.*;
import java.util.Map.Entry;


public class TestOfOrderOfMap {


<span style="white-space:pre">	</span>public void testOfKeyset(Map map, Object obj[]){
 <span style="white-space:pre">		</span>for(int i=0; i<obj.length; i++){
<span style="white-space:pre">			</span>map.put(obj[i], obj[i]);
 <span style="white-space:pre">		</span>}

 <span style="white-space:pre">		</span>System.out.println("LinkedHashMap" +": " + map);
 
 <span style="white-space:pre">		</span>System.out.print("通路      " + map.get(7)+" 之後:   ");
 <span style="white-space:pre">		</span>System.out.println(map);
 <span style="white-space:pre">		</span>System.out.print("通路      " + map.get(10)+" 之後: ");
 <span style="white-space:pre">		</span>System.out.println(map);
 
 <span style="white-space:pre">		</span>System.out.print("最後key: ");
 <span style="white-space:pre">		</span>Iterator it =  map.keySet().iterator();
     <span style="white-space:pre">		</span>while (it.hasNext()) {
      <span style="white-space:pre">		</span>Object key = it.next();
            <span style="white-space:pre">	</span>System.out.print(key+" ");
        <span style="white-space:pre">	</span>} 
         <span style="white-space:pre">	</span>System.out.println();
 <span style="white-space:pre">	</span>}
 
 <span style="white-space:pre">	</span>public static void main(String[] args) {
 <span style="white-space:pre">		</span>TestOfOrderOfMap test = new TestOfOrderOfMap();
 <span style="white-space:pre">		</span>LinkedHashMap lhm1 = new LinkedHashMap(16, 0.75f,true); 
 <span style="white-space:pre">		</span>Integer a[] = new Integer[]{7, 0, 5, 10, 17, 6, 2, 8, 3, 31};
 
 <span style="white-space:pre">		</span>test.testOfKeyset(lhm1, a);
 <span style="white-space:pre">	</span>}
}
           

結果為

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖9

分析:插入的順序為  7, 0, 5, 10, 17, 6, 2, 8, 3, 31        map.get(7)之後,也就是通路7之後,就把7放在了連結清單的最末尾,也就是放在了31之後,變成了

0,5,10,17,6,2,8,3,31,7      接着map.get(10)之後,也就是通路10之後,就把10放在了連結清單的最末尾,也就是7之後,變成了   0,5,17,6,2,8,3,31,7,10

這就是按通路順序存儲,據說這樣的好處就是可以實作LRU,即把所有通路過的對象放在了最後,那麼沒有被通路過的對象就在最前面了,當記憶體不夠用時,

就可以直接把前面最近最久沒有使用的對象删除以接受新對象,進而實作置換。

下面附加一個小知識,

當構造方法的參數 accessOrder為true時,LinkedHashMap的get()方法會改變資料連結清單,那會有什麼後果呢?先看結果

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

圖10

啊哦,報錯了,java.util.ConcurrentModificationException,先說明下,當用Entryset 來周遊的時候就不會有錯,用keyset 周遊的時候就會有錯,就是下面這段代碼

<span style="white-space:pre">		</span>Iterator it =  map.keySet().iterator();
		while (it.hasNext()) {
    		<span style="white-space:pre">	</span>Object key = it.next();
       	 	 <span style="white-space:pre">	</span>System.out.print(key+" ");
       	 	 <span style="white-space:pre">	</span>System.out.print(map.get(key));
   		}	
           

這是為什麼呀,根據報錯的源代碼就很好了解了

<span style="white-space:pre">	</span>nextEntry() {
            if (modCount != expectedModCount) //Entry 被修改的次數不等于它期望的被修改次數,是以抛出了ConcurrentModificationException異常
                throw new ConcurrentModificationException();
            if (nextEntry == header)
                throw new NoSuchElementException();

            Entry<K,V> e = lastReturned = nextEntry;
            nextEntry = e.after;
            return e;
        }
           

因為當 accessOrder為true時, LinkedHashMap的get( )方法會改變資料連結清單,因為每通路一次都要把這個key對應的Entry放到連結清單的最末尾(上面已經分析過),而Keyset 的 Iterator對象不允許我們動态去改變LinkedHashMap的資料連結清單結構,正如上面的源代碼所說的,Entry 被修改的次數不等于它期望的被修改次數。因為本來就是排好序的了,你現在每通路一個,就把它放在最末尾,那麼我接下來去找它的下一個的時候卻發現原來那個已經不在了,因為這是動态發生的,誰也察覺不到,隻有當通路到它的時候才發現不在那個位置了(拿上面那個例子,通路第一個位置上7的時候還是沒問題的,通路完之後,把7放到最後面,原來的第二個位置對應的0,現在被頂到了第一個位置上,那麼我下一輪該通路0的時候卻發現0不見了,換成了5,那麼是不是就應該報錯呢)。現在明白了吧。就好比我用書包背十個乒乓球去體育館打球,突然過來一個同學偷偷摸摸拿走了一個,還把一個壞了的放我書包裡面,也不跟我打聲招呼,我也沒看見,過了一個小時,我把其中的9個乒乓球都打壞了,想拿第十個來用,發現,咦,怎麼是壞的啊,誰換了我的乒乓球啊,我這時是不是該報警啊(哈哈,開個玩笑)

那為什麼accessOrder 為false時,采用keyset周遊就不會出現這種異常啊,因為為false 時,是按插入順序存儲啊,沒有破壞原來的順序啊,也就不會出現

modCount   !=  expectdModCount 的情況了。

 那為什麼entryset周遊的時候就不會出錯呀?因為,keyset 隻是預先把所有的key 放到了set當中,沒有跟value對應起來,

而entryset是把預先所有的Entry 都放在了set中,Entry中有key對應的value,是以key改變的時候value 也會跟着改變,就不會發現錯位的情況了。

兩個的源碼比較

private class KeyIterator extends LinkedHashIterator<K> {
        public K next() { return nextEntry().getKey(); }
    }
           
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() { return nextEntry(); }
    }
           

其中next()裡面的return,在KeyIterator類中還多了一個getKey()。

記得啊,你要犯這一個錯誤得要有三個條件才行呢,1,accessOrder  設為true。2,用keyset 周遊。3,使用map.get(key)方法

到此,最開始提出的三個問題都已經解決了。

那麼回過頭來,我們來測試當key為String  對象的時候又是個什麼情形呢,其實也是大同小異啦

package testCollections;

import java.util.*;
import java.util.Map.Entry;

public class TestOfOrderOfMap {

	public void testOfKeyset(Map map, Object obj[]){
		for(int i=0; i<obj.length; i++){
			map.put(obj[i], obj[i]);
		}
		
		if(map instanceof LinkedHashMap){//LinkedHashMap也是一種HashMap,是以對其測試要放在前面
			System.out.println("LinkedHashMap" +": " + map);
		}else if(map instanceof HashMap){
			System.out.println("HashMap" +": " + map);
		}else if(map instanceof Hashtable){
			System.out.println("Hashtable" +": " + map);
		}else if(map instanceof TreeMap){
			System.out.println("TreeMap" +": " + map);
		}
		
		System.out.print("key: ");
		Iterator it =  map.keySet().iterator();
    	 <span style="white-space:pre">	</span>while (it.hasNext()) {
        	 <span style="white-space:pre">	</span>Object key = it.next();
           	 <span style="white-space:pre">	</span>System.out.print(key+" ");
       	 <span style="white-space:pre">	</span>}		
       <span style="white-space:pre">		</span>System.out.println();
	}
	
	 public static void main(String[] args) {
		 TestOfOrderOfMap test = new TestOfOrderOfMap();
		 
		 HashMap hm2= new HashMap();					//第二組集合是用來測試String類型的key
		 Hashtable ht2 = new Hashtable();	 
		 TreeMap tm2 = new TreeMap();
		 LinkedHashMap lhm2 = new LinkedHashMap(10, 0.75f, true);		
		 
		//Integer a[] = new Integer[]{7, 0, 5, 10, 17,6,2,8,3,31};
		 String a[] = new String[]{"aa", "Ok", "YOU", "abc", "67k"};
		 
		 //測試Keyset中key為數字的排序
		 System.out.println("這裡是KeySet的測試:");
		 test.testOfKeyset(hm2, a);
		 test.testOfKeyset(ht2, a);
		 test.testOfKeyset(tm2, a);
		 test.testOfKeyset(lhm2, a);
		 
		/*//測試Entryset中key為數字的排序
		 System.out.println();
		 System.out.println("這裡是EntrySet的測試:");
		 test.testOfEntryset(hm2, a);
		 test.testOfEntryset(ht2, a);
		 test.testOfEntryset(tm2, a);
		 test.testOfEntryset(lhm2, a);*/
		 
	 }
}
           

結果

論HashMap、Hashtable、TreeMap、LinkedHashMap的内部排序

  圖11

分析,HashMap 跟 Hashtable 一樣都是無章可循的,根據它自己的hashCode來存儲,TreeMap呢,根據key 的第一個字母進行排序,先數字到大寫字母再到小寫字母。

LinkedHashMap 就還是跟上面的分析一樣。完畢!

總結:

1、HashMap、Hashtable的存儲順序跟key 所對應的hashCode有關,但是有小規律,當key的值不超過16時,HashMap的存取是按照從小到大的順序來存取的, 當key的個         數不超過11且key的值不超過11時,Hashtable的存取是按照從大到小的順序來存取的。TreeMap 的順序按照從小到大,LinkedHashMap的順序有兩種,一種是按插入順           序,一種是按通路順序,取決于accessOrder是否為true。

2、keyset 跟 entryset 的周遊結果沒有差別,有一點點差別是在LinkedHashMap中當按通路順序存儲時,采用keyset 周遊 get()方法會報錯。

3、HashMap 的預設大小為16,每次增長會加倍,Hashtable的預設大小為11,每次增長原來的2倍加1,TreeMap 跟 LinkedHashMap暫時無法确定或者說無限大,不過在這 裡不影響我們的了解。

天快亮了,人生第一次寫這麼長的部落格,有什麼纰漏的地方還望各位不吝賜教,畢竟一個人的思維深度是有限的,3Q噢。(還有不得不說CSDN寫部落格的編輯框排版很有問題)