Java2中的集合框架是廣爲人知的,本文打算從幾個方面來講說本身對這個框架的理解。css
下圖是java.util.Collection的類圖(基本完整,有些接口如集合類均實現的Cloneable、Serializable沒有包含進去)html
咱們常說要繼承的話,究竟是寫個抽象類仍是接口,它們區別在於:若是子類確實是父類的一種,應該使用抽象類,描述是「is-a」的關係,而接口則表示一種行爲,描述的是「like-a」的關係。但在Java類庫裏,其實許多原則因爲各類緣由被打破了,好比在Collection框架裏,List/Set都是Collection的一種,爲何不把Collection定義爲抽象類呢?而ArrayList/LinkedList也都是List的一種,爲何不把List定義爲抽象類呢?這就是原則和實際的折衷。做爲Java類庫而言,不只要考慮面向對象的一些原則,也要考慮擴展性和語言自己的限制。能不能把Collection接口去掉,用AbstractCollection做爲頂層?做爲類庫而言是不能夠的,由於Java是單繼承的,若是把AbstractCollection做爲頂層,那麼當用戶自定義的類既要繼承本身的父類,又要具有集合的屬性,那麼就作不到了(能夠自定義集合接口,但就沒法與Collection相互轉化)。所以,Java集合框架採起的是類庫普遍使用的接口+抽象類的形式,以同時得到接口和抽象類的好處,因此咱們看到ArrayList extends AbstractList implements List(AbstractList自己就是實現List的,這裏再寫出implements List是爲了使ArrayList的類結構更爲清晰)。java
另外咱們再看Set接口,它的方法基本和Collection方法如出一轍,爲何要再寫一遍?一方面是做爲類庫而言要增長詳細註釋,雖然是同名的方法但實現的約束不一樣,好比Set的add方法是不會保存重複值的,另外一方面是爲了從Set接口自己能很清楚地看到它所提供的功能(好比size()方法,和Collection是徹底一個含義,也從新定義了一遍),這是從類庫易讀性來考慮,對於咱們本身編寫的類,基本就不須要這樣。算法
說多了,回到集合框架自己。數據庫
Iterable基本是個標識接口,同時約定了全部線性集合(數組、隊列、棧這種一維的都屬於線性集合,Map就屬於二維,不要求遍歷)必須是能夠遍歷的(集合要給出遍歷結構),同時提供了配套的Iterator頂級接口,實現hasNext()、next()和remove()方法來完成遍歷功能。爲何這裏要定義remove接口方法卻不定義add/set方法?我的以爲這多是考慮在類庫的使用過程當中remove的頻率更高,而add的方法頻率要低,set的使用場景就更少了。數組
ListIterator相比Iterator就多提供了不少功能,包括上面提到的add/set,還有得到索引的nextIndex、previousIndex、以及往回迭代的hasPrevious()/previous()。給針對線性表的操做者更多的便利,事實上在AbstractList裏就提供了iterator()和listIterator()兩種方法來提供給開發者更多選擇。相應的,在HashMap裏頭,也提供了實現Iterator接口的HashIterator內部抽象類,而在Apache Commons Collections下甚至單獨寫出MapIterator extends Iterator,因而可知,做爲類庫的設計者,在Iterator和ListIterator/HashMapIterator上是作了便捷性/易用性以及使用場景上的權衡的。安全
ArrayList內部結構是個數組,默認是10,在建立ArrayList對象時此數組是空的(Object[] EMPTY_ELEMENTDATA = {})只有當add的時候才擴容(若是擴容容量小於DEFAULT_CAPACITY,也就是10,就一次性擴容到10)。其擴容的機制是:當前數組容量已經沒法放入更多元素的時候,增長原有數組的一半,數據結構
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);
此數組的最大值是Integer.MAX_VALUE,也就是2^31-1。數組擴容的時候要考慮到當容量再度擴容一半的時候會越界,因此單獨作了判斷處理。數組擴容是在調用了本地方法去分配新的空間區域(下面是Arrays.CopyOf的代碼)併發
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
按照理論上來講,對於能預先知道數組大小的,應該在定義ArrayList的時候指定其容量以減小擴容次數,可是通過如下代碼試驗(虛擬機64位Client模式,JDK1.7.0_45)app
int times=1000000;
long startTime=new Date().getTime();
List<Integer> arrayList=new ArrayList<Integer>(times);
for(int i=0;i<times;i++) {
arrayList.add(i);
}
long endTime=new Date().getTime();
System.out.println("ArrayList增長"+times+"次,耗費時間="+(endTime-startTime));
對於1百萬次增長,使用new ArrayList<Integer>(times)時耗費時間是90ms,而使用new ArrayList<Integer>()的耗費時間居然要短一些,只要81ms;把times擴大到1千萬的時候,差距更明顯,指定容量的要5秒多,而不指定容量的只要3秒多。具體是哪慢了很差下結論,推測應該是當實際元素較少的時候,大數組在尋址、計算等方面要慢一些,反過來講System.arraycopy的效率並無傳說中的那麼低,這也是爲何用的本地方法的緣由。
LinkedList咱們知道內部結構是個線性鏈表,首先看它繼承的不是AbstractList,而是繼承自AbstractSequentialList,這是AbstractList的子類,實現了線性鏈表的骨架方法,如get/set,均是經過ListIterator迭代器來遍歷實現。爲何要創造出AbstractSequentialList這個類?由於線性的不僅有鏈表,但線性的都只有經過迭代器才能找到元素,與之對應的是隨機讀取——也就是數組,所以在AbstractSequentialList的類註釋裏明確說明:若是是隨機讀取的,則使用AbstractList更合適(AbstractList並無提供隨機讀取的實現,類註釋的意思只是說如要隨機讀取,則AbstractSequentialList沒有任何幫助,不如實現AbstractList更準確)。事實上,爲了代表集合是否能夠根據索引隨機讀取,Collection框架專門定義了一個空接口RandomAccess,以標識該類是否可隨機讀,ArrayList、Vector都實現了這個接口,而沒有實現這個接口的,則是不能夠經過下標索引來尋址的。
LinkedList有比ArrayList在接口上有更豐富的功能,好比addFirst()、addLast()、push()、pop(),、indexOf(),同時它的listIterator()也要比iterator()更經常使用一些。咱們之前常說對於常常刪除、增長的集合,使用LinkedList比ArrayList效率要高,這是容易被誤解的,LinkedList的尋址相比數組來講很是地慢,若是在頻繁增/刪以前須要尋址定位,那麼仍然比ArrayList要慢不少,數十倍地慢,因此使用它的時候要謹慎,不能耍小聰明。LinkedList根據索引尋址的get(int index)方法,使用的是簡單的「二分法」,即若是index小於size的一半,則從前日後迭代;大於size一半則從後往前迭代。這也是沒有辦法的事情,LinkedList是須要保證插入順序的,因此不能作任何排序,也就不能使用任何如冒泡、快速排序之類的算法。有沒有不須要保證插入順序從而可以快速尋址的集合呢?TreeSet/HashSet能夠快速尋址,但不能有重複值;TreeMap/HashMap一樣是不能有重複值;Collection框架並無給出能有重複值同時又能容許排序的List,應該是他們認爲ArrayList就能夠知足這種場景了,但類庫中有個類IdentityHashMap,它的hash()方法用的是System.identityHashCode()而不是HashMap所用的key.hashCode。System.identityHashCode()意思是無論對象是否實現了hashCode,都取Object的hashCode也就是對象的內存地址來做爲key,這樣即便兩個對象hashCode相等,也會被重複插入(在該類的註釋中說到了它的一些使用場景,有興趣的能夠仔細看下)。
咱們知道一般的集合都是非線程安全的,表如今多個線程同時增/刪時,集合大小會不可預測,同時Iterator儘可能保證在迭代過程當中操做是安全的(不保證準確,但儘可能保證不會有越界問題),即當某線程迭代讀取集合時,若有其餘線程修改此集合的結構(擴大/縮小),則會拋出ConcurrentModificationException。那麼它是如何實現的呢?在集合中都會維護一個內部計數器modCount,若是有影響集合結構的操做(增長、刪除、合併等,而修改不是),modCount都會自增1。在對集合迭代時,都會檢查當前迭代時的操做計數器副本expectedModCount(迭代前初始化爲和modCount相等)和modCount是否相等
int expectedModCount = modCount;
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
初始擴容的容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,在擴容的過程當中同時會計算下次擴容的閾值threshold,它=數組大小*負載因子。爲何在達到threshold的時候擴容而不是在達到數組最大長度的時候?這是爲了減小每一個數組元素上的Entry數,由於根據hash()方法,在把table數組佔滿以前,極可能在其餘元素上已經有多個了(從機率角度),但負載因子又不能過小,不然會形成不少空間浪費,因此做者權衡(這裏可能也是根據hash()或某些數學原理)取0.75做爲負載因子,即達到table數組3/4時就擴容,而且是擴容2倍,不是ArrayList那樣擴容一半
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);
}
createEntry(hash, key, value, bucketIndex);
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
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);
return h ^ (h >>> 7) ^ (h >>> 4);
}
至於爲何這麼作,比較複雜,我也沒搞太清,尤爲是其中爲何選擇20、十二、七、4這樣的來右移。還有hashSeed的選擇也不太清楚。
這裏必需要提下HashMap擴容的效率問題。前面提到ArrayList的擴容性能並不差,而HashMap就徹底不同了,經實驗,擴容至少帶來性能降低1半以上,但有臨界點,元素超過10萬數量級差距就不明顯了。下面是代碼和測試結果
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class ResizePerformanceTest {
public static void main(String[] args) {
run(1000);
run(10000);
run(100000);
run(1000000);
run(10000000);
}
public static void run(int times) {
System.out.println("增長"+times+"次");
long startTime=new Date().getTime();
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
for(int i=0;i<times;i++) {
map.put(i, i);
}
long endTime=new Date().getTime();
System.out.println("HashMap自動擴容的方式,增長"+times+"次,耗費時間="+(endTime-startTime));
long startTime1=new Date().getTime();
Map<Integer,Integer> map1=new HashMap<Integer,Integer>(times);
for(int i=0;i<times;i++) {
map1.put(i, i);
}
long endTime1=new Date().getTime();
System.out.println("HashMap預先指定空間的方式,增長"+times+"次,耗費時間="+(endTime1-startTime1));
}
}
增長1000次
HashMap自動擴容的方式,增長1000次,耗費時間=2
HashMap預先指定空間的方式,增長1000次,耗費時間=1
增長10000次
HashMap自動擴容的方式,增長10000次,耗費時間=15
HashMap預先指定空間的方式,增長10000次,耗費時間=6
增長100000次
HashMap自動擴容的方式,增長100000次,耗費時間=25
HashMap預先指定空間的方式,增長100000次,耗費時間=21
增長1000000次
HashMap自動擴容的方式,增長1000000次,耗費時間=1707
HashMap預先指定空間的方式,增長1000000次,耗費時間=1611
增長10000000次
HashMap自動擴容的方式,增長10000000次,耗費時間=21054
HashMap預先指定空間的方式,增長10000000次,耗費時間=17820
HashMap大約就說這麼多,再說說TreeMap。TreeMap是種紅黑樹的結構,可以對元素排序(紅黑樹、數據庫的B樹、B+樹,還有冒泡算法、快速排序算法這些算法領域的,如今還真是不那麼掌握牢固)。爲了保證排序,提供了兩種方式:一種是Key對象實現Comparable接口,另一種方式是單獨提供Comparator實現類
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
若是自己Key對象的排序是肯定的,好比Integer按大小排序,String按照字典排序,這些是無疑義的,因此它們都實現了Comparable接口,但假如說Person對象,有時須要按年齡排序,有時須要按身高排序,有時須要按薪酬排序,因此就沒辦法使用Comparable接口了,此時能夠根據不一樣排序方式建立相應的Comparator類。
既然是按照順序排列的樹,那天然就須要提供一些數據結構方面的方法,因此TreeMap有了firstKey()、lastKey()、pollFirstEntry()、lowerEntry(K)、floorEntry(K)、headMap(K)、tailMap(K)、desendintMap()這樣的方便方法。
相比HashMap,TreeMap還有個很大的不一樣,就是它不只是繼承AbstractMap,還實現了NavigableMap,NavigableMap繼承自SortedMap,SortedMap繼承自Map。SortedMap定義了什麼?firstkey()、lastKey()、headMap(K)、tailMap(K)、subMap(K,K),NavigableMap定義了pollFirstEntry()、lowerEntry(K)、floorEntry(K)等方法。爲何這麼設計?SortedMap是好理解的,針對能夠排序的Map單獨設一個接口,但爲何要NavigableMap呢?它的lowerEntry(K)之類的方法爲何不能合併到SortedMap裏去?我的以爲這應該是兩個版本時期致使的,NavigableMap是JDK1.6時加入的,此時已經有了很多SortedMap的子類,不是頗有必要讓子類也去實現這些方法,因此新加了個NavigableMap類,在須要lower的時候實現它便可,不須要時就直接實現SortedMap。也就是設計這種類庫接口時的粒度問題,基本的方法在上一級接口定義,雖然另一些方法也是正常使用,但根據它的頻率、約束性有所不一樣能夠下放,同時又要考慮不能使接口數量太多加大複雜性。
理解了Map,再來看Set就很簡單了。HashSet內部徹底是以Set元素爲key,new Object()爲value的HashMap
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
它的一些如size()、contain()方法都是直接調用map的方法。
public int size() {
return map.size();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
TreeSet也是同樣,且也有相配套的NavigableSet、SortedSet。
從總體來講,感受Set設計地不是太好,其大多數功能和List很像,僅非重複這個頻率並不高的場景不足以單獨列這一套接口,而其實現上又基本上徹底依託於Map。若是開發者真有這種場景,徹底能夠自行用HashMap來代替。
集合框架中還有兩個頗有用的輔助類,分別是Collections和Arrays,這兩個就很少介紹了。Collections提供了一系列synchronized集合、unmodified集合以及不多用的Checked集合(類型檢查的),以及toArray(toArray(T[] a)更好用,由於能指定返回數組的元素類型)、binarySearch(快速查找算法,須要參數列表元素能排序,不然結果就不許確)。而Arrays提供了一些如sort、merge、binarySearch、copyOf、asList這樣有效的方法,注意這裏的asList返回的Arrays內部實現的一個ArrayList,有些方法不支持,好比add、remove,除了set以外基本上就是一個只讀列表,若是須要可add/remove,仍是須要使用集合的相應構造函數或者Collections的copy方法)。
最後兩個須要說的是雖然是在java.util根目錄下,但基本是爲java.util.concurrent準備的,就是Queue(隊列)和Deque(雙向隊列)。Queue的一系列子類如DelayQueue、LinkedBlockQueue更多地是和併發有關,這放到未來的JUC框架時再講吧。