[轉]Java,java
資料結構是以某種形式將資料群組織在一起的集合,它不僅儲存資料,還支援訪問和處理資料的操作。Java提供了幾個能有效地組織和操作資料的資料結構,這些資料結構通常稱為Java集合架構。在平常的學習開發中,靈活熟練地使用這些集合架構,可以很明顯地提高我們的開發效率,當然僅僅會用還是不夠的,理解其中的設計思想與原理才能更好地提高我們的開發水平。下面是自己對Java集合架構方面的學習總結。
一、概述二、Collection介面 1.List 2.Set 3.Queue三、Map介面 1.HashMap實現原理 2.其它Map實作類別四、其它集合類五、總結
一、概述
在Java 2之前,Java是沒有完整的集合架構的。它只有一些簡單的可以自擴充的容器類,比如Vector,Stack,Hashtable等。這些容器類在使用的過程中由於效率問題飽受詬病,因此在Java 2中,Java設計者們進行了大刀闊斧的整改,重新設計,於是就有了現在的集合架構。需要注意的是,之前的那些容器類庫並沒有被棄用而是進行了保留,主要是為了向下相容的目的,但我們在平時使用中還是應該盡量少用。
Java集合架構
從上面的集合架構圖可以看到,Java集合架構主要包括兩種類型的容器,一種是集合(Collection),儲存一個元素集合,另一種是圖(Map),儲存鍵/值對映射。Collection介面又有3種子類型,List、Set和Queue,再下面是一些抽象類別,最後是具體實作類別,常用的有ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap等等。
二、Collection介面
Collection介面是處理對象集合的根介面,其中定義了很多對元素進行操作的方法,AbstractCollection是提供Collection部分實現的抽象類別。展示了Collection介面中的全部方法。
Collection介面結構
其中,有幾個比較常用的方法,比如方法add()添加一個元素到集合中,addAll()將指定集合中的所有元素添加到集合中,contains()方法檢測集合中是否包含指定的元素,toArray()方法返回一個表示集合的數組。Collection介面有三個子介面,下面詳細介紹。
1.List
List介面擴充自Collection,它可以定義一個允許重複的有序集合,從List介面中的方法來看,List介面主要是增加了面向位置的操作,允許在指定位置上操作元素,同時增加了一個能夠雙向遍曆線性表的新列表迭代器ListIterator。AbstractList類提供了List介面的部分實現,AbstractSequentialList擴充自AbstractList,主要是提供對鏈表的支援。下面介紹List介面的兩個重要的具體實作類別,也是我們可能最常用的類,ArrayList和LinkedList。
ArrayList
通過閱讀ArrayList的源碼,我們可以很清楚地看到裡面的邏輯,它是用數組儲存元素的,這個數組可以動態建立,如果元素個數超過了數組的容量,那麼就建立一個更大的新數組,並將當前數組中的所有元素都複製到新數組中。假設第一次是集合沒有任何元素,下面以插入一個元素為例看看源碼的實現。
1、方法add(E e)向集合中添加指定元素。 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }2、此方法主要是確定將要建立的數組大小。 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); }3、最後是建立數組,可以明顯的看到先是確定了添加元素後的大小之後將元素複製到新數組中。 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
LinkedList
同樣,我們開啟LinkedList的源檔案,不難看到LinkedList是在一個鏈表中儲存元素。
在學習資料結構的時候,我們知道鏈表和數組的最大區別在於它們對元素的儲存方式的不同導致它們在對資料進行不同操作時的效率不同,同樣,ArrayList與LinkedList也是如此,實際使用中我們需要根據特定的需求選用合適的類,如果除了在末尾外不能在其他位置插入或者刪除元素,那麼ArrayList效率更高,如果需要經常插入或者刪除元素,就選擇LinkedList。
2.Set
Set介面擴充自Collection,它與List的不同之處在於,規定Set的執行個體不包含重複的元素。在一個規則集內,一定不存在兩個相等的元素。AbstractSet是一個實現Set介面的抽象類別,Set介面有三個具體實作類別,分別是散列集HashSet、鏈式散列集LinkedHashSet和樹形集TreeSet。
散列集HashSet
散列集HashSet是一個用於實現Set介面的具體類,可以使用它的無參構造方法來建立空的散列集,也可以由一個現有的集合建立散列集。在散列集中,有兩個名詞需要關注,初始容量和客座率。客座率是確定在增加規則集之前,該規則集的飽滿程度,當元素個數超過了容量與客座率的乘積時,容量就會自動翻倍。
下面看一個HashSet的例子。
/** * @author JackalTsc */public class TestHashSet { public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("11111"); set.add("22222"); set.add("33333"); set.add("44444"); set.add("22222"); System.out.println(set.size()); for (String e : set) { System.out.println(e); } }}
從輸出結果我們可以看到,規則集裡最後有4個元素,而且在輸出時元素還是無序的。
鏈式散列集LinkedHashSet
LinkedHashSet是用一個鏈表實現來擴充HashSet類,它支援對規則集內的元素排序。HashSet中的元素是沒有被排序的,而LinkedHashSet中的元素可以按照它們插入規則集的順序提取。
樹形集TreeSet
TreeSet擴充自AbstractSet,並實現了NavigableSet,AbstractSet擴充自AbstractCollection,樹形集是一個有序的Set,其底層是一顆樹,這樣就能從Set裡面提取一個有序序列了。在執行個體化TreeSet時,我們可以給TreeSet指定一個比較子Comparator來指定樹形集中的元素順序。樹形集中提供了很多便捷的方法。
下面是一個TreeSet的例子。
/** * @author JackalTsc */public class TestSet { public static void main(String[] args) { TreeSet<Integer> set = new TreeSet<>(); set.add(1111); set.add(2222); set.add(3333); set.add(4444); set.add(5555); System.out.println(set.first()); // 輸出第一個元素 System.out.println(set.lower(3333)); //小於3333的最大元素 System.out.println(set.higher(2222)); //大於2222的最大元素 System.out.println(set.floor(3333)); //不大於3333的最大元素 System.out.println(set.ceiling(3333)); //不小於3333的最大元素 System.out.println(set.pollFirst()); //刪除第一個元素 System.out.println(set.pollLast()); //刪除最後一個元素 System.out.println(set); }}
3.Queue
隊列是一種先進先出的資料結構,元素在隊列末尾添加,在隊列頭部刪除。Queue介面擴充自Collection,並提供插入、提取、檢驗等操作。
Queue介面結構
中,方法offer表示向隊列添加一個元素,poll()與remove()方法都是移除隊列頭部的元素,兩者的區別在於如果隊列為空白,那麼poll()返回的是null,而remove()會拋出一個異常。方法element()與peek()主要是擷取頭部元素,不刪除。
介面Deque,是一個擴充自Queue的雙端隊列,它支援在兩端插入和刪除元素,因為LinkedList類實現了Deque介面,所以通常我們可以使用LinkedList來建立一個隊列。PriorityQueue類實現了一個優先隊列,優先隊列中元素被賦予優先順序,擁有高優先順序的先被刪除。
/** * @author JackalTsc */public class TestQueue { public static void main(String[] args) { Queue<String> queue = new LinkedList<>(); queue.offer("aaaa"); queue.offer("bbbb"); queue.offer("cccc"); queue.offer("dddd"); while (queue.size() > 0) { System.out.println(queue.remove() + ""); } }}
三、Map介面
Map,圖,是一種儲存索引值對映射的容器類,在Map中鍵可以是任意類型的對象,但不能有重複的鍵,每個鍵都對應一個值,真正儲存在圖中的是索引值構成的條目。下面是介面Map的類結構。
介面Map的結構
從上面這張圖中我們可以看到介面Map提供了很多查詢、更新和擷取儲存的索引值對的方法,更新包括方法clear()、put()、putAll()、remove()等等,查詢方法包括containsKey、containsValue等等。Map介面常用的有三個具體實作類別,分別是HashMap、LinkedHashMap、TreeMap。
1.HashMap
HashMap是基於雜湊表的Map介面的非同步實現,繼承自AbstractMap,AbstractMap是部分實現Map介面的抽象類別。在平時的開發中,HashMap的使用還是比較多的。我們知道ArrayList主要是用數組來儲存元素的,LinkedList是用鏈表來儲存的,那麼HashMap的實現原理是什麼呢?先看下面這張圖:
HashMap原理.jpg
在之前的版本中,HashMap採用數組+鏈表實現,即使用鏈表處理衝突,同一hash值的鏈表都儲存在一個鏈表裡。但是當鏈表中的元素較多,即hash值相等的元素較多時,通過key值依次尋找的效率較低。而JDK1.8中,HashMap採用數組+鏈表+紅/黑樹狀結構實現,當鏈表長度超過閾值(8)時,將鏈錶轉換為紅/黑樹狀結構,這樣大大減少了尋找時間。
下面主要通過源碼介紹一下它的實現原理。
HashMap儲存元素的數組
transient Node<K,V>[] table;
數組的元素類型是Node<K,V>,Node<K,V>繼承自Map.Entry<K,V>,表示索引值對映射。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //建構函式 ( Hash值索引值下一個節點 ) Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
接下來我們看下HashMap的put操作。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果沒有初始化則初始化table if ((p = tab[i = (n - 1) & hash]) == null) //這裡 (n-1)&hash 是根據hash值得到這個元素在數組中的位置(即下標) tab[i] = newNode(hash, key, value, null); //如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上 else { Node<K,V> e; K k; //第一節節點hash值同,且key值與插入key相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //屬於紅/黑樹狀結構處理衝突 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { /鏈表處理衝突 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //新增節點後如果節點個數到達閾值,則將鏈錶轉換為紅/黑樹狀結構 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //更新hash值和key值均相同的節點Value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
接下來我們看下HashMap的get操作。
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //如果第一個節點是TreeNode,說明採用的是數組+紅/黑樹狀結構結構處理衝突 //遍曆紅/黑樹狀結構,得到節點值 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
到這裡HashMap的大致實現原理應該很清楚了,有幾個需要關注的重點是:HashMap儲存元素的方式以及根據Hash值確定映射在數組中的位置還有JDK 1.8之後加入的紅/黑樹狀結構的。
在HashMap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。對於任意給定的對象,只要它的hashCode()傳回值相同,那麼程式調用hash(int h)方法所計算得到的hash碼值總是相同的。我們首先想到的就是把hash值對數組長度模數運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在HashMap中,(n - 1) & hash用於計算對象應該儲存在table數組的哪個索引處。HashMap底層數組的長度總是2的n次方,當數組長度為2的n次冪的時候,(n - 1) & hash 算得的index相同的幾率較小,資料在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍曆某個位置上的鏈表,這樣查詢效率也就較高了。
2.LinkedHashMap
LinkedHashMap繼承自HashMap,它主要是用鏈表實現來擴充HashMap類,HashMap中條目是沒有順序的,但是在LinkedHashMap中元素既可以按照它們插入圖的順序排序,也可以按它們最後一次被訪問的順序排序。
3.TreeMap
TreeMap基於紅/黑樹狀結構資料結構的實現,索引值可以使用Comparable或Comparator介面來排序。TreeMap繼承自AbstractMap,同時實現了介面NavigableMap,而介面NavigableMap則繼承自SortedMap。SortedMap是Map的子介面,使用它可以確保圖中的條目是排好序的。
在實際使用中,如果更新圖時不需要保持圖中元素的順序,就使用HashMap,如果需要保持圖中元素的插入順序或者訪問順序,就使用LinkedHashMap,如果需要使圖按照索引值排序,就使用TreeMap。
四、其它集合類
上面主要對Java集合架構作了詳細的介紹,包括Collection和Map兩個介面及它們的抽象類別和常用的具體實作類別,下面主要介紹一下其它幾個特殊的集合類,Vector、Stack、HashTable、ConcurrentHashMap以及CopyOnWriteArrayList。
1.Vector
前面我們已經提到,Java設計者們在對之前的容器類進行重新設計時保留了一些資料結構,其中就有Vector。用法上,Vector與ArrayList基本一致,不同之處在於Vector使用了關鍵字synchronized將訪問和修改向量的方法都變成同步的了,所以對於不需要同步的應用程式來說,類ArrayList比類Vector更高效。
2.Stack
Stack,棧類,是Java2之前引入的,繼承自類Vector。
3.HashTable
HashTable和前面介紹的HashMap很類似,它也是一個散列表,儲存的內容是索引值對映射,不同之處在於,HashTable是繼承自Dictionary的,HashTable中的函數都是同步的,這意味著它也是安全執行緒的,另外,HashTable中key和value都不可以為null。
上面的三個集合類都是在Java2之前推出的容器類,可以看到,儘管在使用中效率比較低,但是它們都是安全執行緒的。下面介紹兩個特殊的集合類。
4.ConcurrentHashMap
Concurrent,並發,從名字就可以看出來ConcurrentHashMap是HashMap的安全執行緒版。同HashMap相比,ConcurrentHashMap不僅保證了訪問的執行緒安全性,而且在效率上與HashTable相比,也有較大的提高。關於ConcurrentHashMap的設計,我將會在下一篇關於並發編程的部落格中介紹,敬請關注。
5.CopyOnWriteArrayList
CopyOnWriteArrayList,是一個安全執行緒的List介面的實現,它使用了ReentrantLock鎖來保證在並發情況下提供高效能的並發讀取。
五、總結
到這裡,對於Java集合架構的總結就結束了,還有很多集合類沒有在這裡提到,更多的還是需要大家自己去查去用。通過閱讀源碼,查閱資料,收穫很大。
Java集合架構主要包括Collection和Map兩種類型。其中Collection又有3種子類型,分別是List、Set、Queue。Map中儲存的主要是索引值對映射。
規則集Set中儲存的是不重複的元素,線性表中儲存可以包括重複的元素,Queue隊列描述的是先進先出的資料結構,可以用LinkedList來實現隊列。
效率上,規則集比線性表更高效。
ArrayList主要是用數組來儲存元素,LinkedList主要是用鏈表來儲存元素,HashMap的底層實現主要是藉助數組+鏈表+紅/黑樹狀結構來實現。
Vector、HashTable等集合類效率比較低但都是安全執行緒的。包java.util.concurrent下包含了大量安全執行緒的集合類,效率上有較大提升。
作者:塵語凡心
連結:http://www.jianshu.com/p/63e76826e852
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。