標籤:java集合 資料結構
集合類型
Set集合:集合元素是不能重複的。元素是沒有順序的。所以它不能基於位置訪問元素。TreeSet和HashSet是它的實作類別。
List集合: 集合元素是可以重複的。元素是有順序的。所以它可以基於位置訪問元素。ArrayList和LinkedList是它的實作類別。
Map:它包含索引值對。Map的鍵是不能重複的。Map不能保證儲存的順序。HashMap和TreeMap是它的實作類別。
怎樣來選擇?
事實上,選擇Set,List或者Map是依賴你的資料結構的。如果你將要儲存的資料沒有重複且不需要順序,你可以選擇用Set。如果你將要儲存的資料需要保證順序,你可以選擇用List。如果你有一個索引值對來關聯兩個不同的對象或者用一個標識符來標識對象,那麼你可以選擇Map。
舉個例子:
顏色的集合最好放入Set中。
球隊最好放在List中。因為球隊的出場需要順序。
Web Sessions的集合最好在Map上;唯一的session ID會更好的引用實際對象。
當我們選擇用哪個集合的時候,我們主要關心的是集合的速度:
- 訪問元素的速度
- 添加一個新元素的速度
- 移除一個元素的速度
- 迭代的速度
此外, 也有一致性的問題。一些實現會保證訪問的速度,而一些實現將有個變化的速度。我們關心的這些速度依賴於集合的具體實現。
鏈表實現
具體的類:LinkedList, LinkedHashSet
內部原理:每個節點中都持有一個元素和下一個元素的指標。如:
- 如果我們想加入一個元素到的第二個位置上,是很簡單的。就像一樣,它只須把原圖中的第一個節點中的指標指向新加入的這個元素,把新加入的這個元素的指標指向原圖中的第二個節點就可以了。這個速度是非常快的!不需要拷貝,移動和記錄原集合中的元素。
- 移除元素也是同理的,只要把原圖中第一個節點中的指標指向原圖中第二個節點的元素就可以了。
當我們想訪問集合中的元素是很慢的。先看看LinkedList的源碼:
/** * 返回指定索引位置的元素 */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
當我們想擷取指定位置的一個元素時,程式會先將集合大小size右移一位(也就是說,把size的大小會減少一半),然後判斷索引位置是離第一個節點位置近還是最後一個節點位置近,然後開始遍曆,得到指定的節點。
舉個例子:如果集合中有50個元素,如果你想擷取第20個元素,那麼它會從集合中第一個元素開始遍曆,直到擷取到第20個元素為止;如果你想擷取第40個元素,那麼它會從集合中最後一個元素開始遍曆,直到擷取到第40個元素為止。
所以對於鏈表實現的集合來說,它訪問元素的速度很慢。而且訪問不同位置的元素,速度還不一致。還有一點我們要深深牢記的是:當我們做添加和移除操作時,都會調用到上面的node方法,從而遍曆擷取所要動作節點的前一個節點,因此,這會對速度有一定的影響。
因此,當你需要更快的添加/移除,而且並不怎麼關心訪問時間的時候,像LinkedList這樣的鏈表集合是更合適的。如果你打算在你的集合中有很多的添加/移除元素操作,那麼這是個不錯的選擇。
數組實現
具體的類:ArrayList
ArrayList是集合類中的唯一基於數組實現的。
看:在ArrayList中,當我們把一個新元素添加到第四個位置上時,它會把第四個位置(包括第四個位置)以後的每一個位置上的元素都向後挪一位,然後在把新加入的元素插入到第四個位置上。這是很慢的,而且這並不能保證時間,它依賴於有多少的元素需要拷貝。同理,移除一個元素就是把後面的元素全部向前挪一位。
還有一種更糟糕的情況。當我們建立ArrayList對象的時候,它的數組長度是固定的(這個長度可以在建構函式中設定,如果不設定預設為10)。當我們操作集合的時候,如果它的容量超過這個固定長度的話,就不得不建立一個更大容量的數組,然後把當前集合中的所有元素拷貝到新建立的這個集合當中。這是非常非常慢的。
然而,ArrayList訪問元素的速度是很快的。對於數組來說,它在記憶體空間中的位置是連續的,所以我們不需要遍曆整個集合就可以準確的算出元素引用在記憶體中的位置。並且,耗費的時間也是一致的。
因此,如果你有一些並不怎麼修改,而且需要快速存取的元素,那麼ArrayList是一個很好的選擇。
Hash實現
具體的類:HashSet, HashMap
HashSet是基於HashMap實現的,所以這裡我就只解釋HashMap了。
HashMap的工作原理:HashMap底層就是一個數組結構(叫做Entry Table),數組中的每一項又是一個鏈表(叫做Bucket)。當建立一個HashMap的時候,就會初始化一個數組。
public V put(K key, V value) { ...... int hash = hash(key); 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; } } ...... addEntry(hash, key, value, i); return null; }
上面是HashMap中put方法的部分代碼(JDK7)。當我們向HashMap中put元素的時候,先根據key重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),再通過這個下標訪問到Bucket的鏈頭並開始遍曆整個Bucket(也就是整個鏈表),如果Bucket中已經存在新添加的key的值,則將原有的值設定成新添加的值,並返回舊值。否則,新加入的元素放在鏈頭,最先加入的放在鏈尾。
總的來說,程式首先根據準備放入的元素的key(通過hash演算法)決定該Entry在Entry Table中的儲存位置。如果新加入的這個Entry 的key與原有Entry的Key通過equals比較返回 true,那麼新添加 Entry的value將覆蓋集合中原有Entry的value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部(也就是Bucket的鏈頭)。
同理,從HashMap中get元素時,首先通過key計算出的Hash值來確定Entry在Entry Table中的儲存位置,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。
總結:HashMap 底層採用一個 Entry[] 數組來儲存所有的Entry對象(裡麵包含hash值、key-value對、下一個Entry的指標),當需要儲存一個 Entry 對象時,會根據hash演算法來決定其在數組中的儲存位置,在根據equals方法決定其在該數組位置上的鏈表中的儲存位置;當需要取出一個Entry時,也會根據hash演算法找到其在數組中的儲存位置,再根據equals方法從該位置上的鏈表中取出該Entry。
HashMap的rehashing
當HashMap中的元素越來越多的時候,hash衝突的幾率也就越來越高,因為數組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數組進行擴容。數組容量將會自動擴大兩倍,在數組擴容時,所有原存在的Entry會重新計算索引值,並且Entry鏈的順序也會發生顛倒(如果還在同一個鏈中的話),而該新添加的Entry的索引值也會重新計算,這是最消耗效能的,這個過程就是rehashing。
那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組小loadFactor時,就會進行數組擴容,loadFactor的預設值為0.75,這是一個折中的取值。也就是說,預設情況下,數組大小為16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴充為 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效提高HashMap的效能。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Java集合資料結構