畢業兩個星期了,開始成爲一名正式的java碼農了。
一直對偏底層比較感興趣,想着深刻本身的java技能,看書、讀源碼、總結、造輪子實踐都是付諸行動的方法。
說到看源碼,就應該由簡入難,逐漸加深,那就從jdk的源碼開始看起吧。java
ArrayList和Vector是java標準庫提供的一種比較簡單的數據結構,也是最經常使用的一種。算法
<!-- more -->數組
表這種抽象概念指的是一種存放數據的容器,其中數據A1, A2, A3, ..., Ai, ... AN有序排列。安全
那麼在表這一抽象的數據結構模型中:數據結構
它的操做有:併發
表有兩種實現方式,數組實現和鏈表實現。這兩種實現方式各有優劣,其中這裏所說的ArrayList是數組實現方式。jvm
線性表的實現中是將元素放到數組中的。若是是C的數組,那實際上是一塊連續的內存。
在java中也是java的原生數組,內存佈局中邏輯上是連續的,物理上不必定。印象中有的jvm實現爲了解決內存碎片搞相似邏輯內存的這種操做。函數
往線性表取出數據或拿出元素都能很直接對應到原生數組中去。
可是,線性表插入數據的這一操做要求表是可以動態增加的,可是原生數組的大小是固定的。源碼分析
線性表實現的一大重點是實現表動態增加這一要求,將其轉換、化歸到固定大小的原生數據上去。思路是,當線性表內部保存數據的數組若是不夠用了,就申請一個更大的數組,將原來數組的數據拷貝進去。佈局
如上圖所示,在設計上,它的結構應該理解成 ArrayList
--> List
--> Collection
--> Iterable
:
List
接口表示抽象的表,也就是上面所說的表。Collection
接口表示抽象的容器,能放元素的都算。Iterable
接口表示可迭代的對象。也即該容器內的元素都可以經過迭代器迭代。可是在繼承結構上,ArrayList
繼承了AbstractList
。這是java中通用的一種技巧,因爲java8以前的接口沒法指定默認方法,若是你要實現List
接口,你須要實現其中的全部方法。
可是,List
接口中的全部方法是大而全的,有大量方法是爲了方便調用者使用而設計的衍生方法,並不是實現該接口的必須方法。好比說,isEmpty
方法就能夠化歸到size
方法上。
因此,java對於List接口會提供一個AbstractList
抽象類,裏面提供了部分方法的默認實現,而繼承該類的只須要實現一組最小的必須方法便可。
總而言之一句話,AbstractList
的做用是給List
接口提供某些方法的默認實現。
先看ArrayList內部保存的屬性和狀態。
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access private int size;
其中,最關鍵的爲elementData
和size
。 elementData
也即真正存儲元素的java原生數組。
因爲ArrayList中實際保存的元素個數是少於elementData
數組的大小的,也即會有部分空間的浪費,所以size
屬性是用來保存ArrayList
存儲的元素個數。
值得注意的是,elementData
沒有訪問修飾符,咱們知道默認是對包內可見的。這樣作的目的是:
構造函數一共有三個,下面是其中兩個。第三個是從其它容器初始化,沒什麼好看的地方。
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
第一個帶參數的構造函數,行爲上這個都知道,建立出來的依然是個空表,看代碼也可發現執行完後size
屬性爲0。initialCapacity
的含義是指定內部存儲數據的原生數組的初始化大小。
代碼邏輯很簡單,initialCapacity
大於0時對elementData
分配該大小的原生數組。可是問題是,initialCapacity
爲0時,並非直接new一個大小爲0的數組,而是使用靜態變量EMPTY_ELEMENTDATA
來代替。
爲何?EMPTY_ELEMENTDATA
是靜態變量,全部實例共享,我的猜想應該是爲了省點內存吧。但是這個佔不了多大的內存啊,這個理由可能說服力不太強。
第二個默認構造函數,這個函數行爲上也是建立空表。可是會預先將存儲數據的原生數組的大小設置爲DEFAULT_CAPACITY
,也就是默認值10。這樣,能避免在大小較小時頻繁擴容帶來性能損耗。
按照這個思路,代碼應該長這樣纔對:
public ArrayList() { this.elementData = new Object[DEFAULT_CAPACITY]; }
可是實際上,咱們發現DEFAULTCAPACITY_EMPTY_ELEMENTDATA
實際上是個空表,也是靜態變量,多實例共享的。
這實際上也能夠認爲是一種優化手段,由於不少場景都是直接默認構造的一個空表在哪裏放着,爲了節約內存,jdk實現先不實際分配空間,僅僅作一個相似標記做用的操做,以後真正使用了纔會分配空間,達到一個相似「延遲分配」的效果。這些思路在擴容相關的代碼中有所體現。
get和set沒什麼好說的,其實是直接操做內部的原生數組。
不過,從下面的代碼中能夠看到,在get和set以前,會作越界檢查。
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; } public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
下面再來看插入元素。插入有兩種:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
思路都特別顯然,主要是在操做以前,調用了ensureCapacityInternal
函數,這個函數調用完畢後會保證內部數組的空間能存儲下這麼多元素。
此外,void add(int, E)
函數還作了越界檢查。
接下來看ensureCapacityInternal
函數,相關實現涉及到四個函數。
這個函數的目的是保證執行完後,內部的原生數組至少能容納minCapacity
個元素。
以前所說的那種「延遲分配」操做在這裏就體現出來了。分析代碼流程不難發現,當elementData
爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,也即前面所說到的那個標記,那麼以後擴容後的原生數組空間必定不小於DEFAULT_CAPACITY
,也就是前面定義處的10。
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } 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); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
再看具體的擴容邏輯,也即grow
函數。邏輯還不單一。首先擴容嘗試按照原來大小的1.5倍擴容,爲了性能,這裏除以2優化成了右移運算。
若是1.5倍不夠怎麼辦?若是是我,我可能會優雅的遞歸再擴大,然而,jdk的作法是若是1.5倍不夠的話直接按照須要的大小擴容。
最後,若是實在太大,也要作一下限制,最大可達到的大小爲Integer.MAX_VALUE
,不然就超過了int的範圍,溢出了。
下面是移除相關的函數,有兩個:
E remove(int)
是經過索引移除。boolean remove(Object)
是經過元素移除。public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
按索引移除的的思路很簡單,首先把須要移除的元素拿出來,而後後面的都往前挪一個,經過數據拷貝實現。
可是,有個 特別須要注意的地方是,假設刪除以後的表大小爲N,往前挪一個後,elementData[N - 1]
和elementData[N]
都指向了最後一個元素。這時elementData[N]
仍然持有對該元素的引用。若是以後試圖移除表中最後一個元素,它可能不會及時的被gc幹掉,形成無心義的額外內存佔用。
按元素移除的思路也很顯然,先是找到該元素的索引,而後按索引移除。fastRemove
的代碼幾乎和remove(int)
如出一轍,不知道爲何複用一下。。。
最後,從代碼能夠看到一點,表內元素會被移除,可是jdk對ArrayList的實現只會擴容表,而不會縮小表以減少內存佔用。不過,它提供了一個trimToSize
方法,將表中原生數組的空閒空間去掉:
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
很明顯,它從新分配一個正好合適的原生數組,而後拷貝過去。
這兩個函數很好理解:
這兩個函數算法幾乎如出一轍,所以抽象出一個函數batchRemove
來實現。
public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); } public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true); } private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
核心在於try中的那三行代碼。這個算法簡化了就是這樣:
有兩個數組data和isRemove,isRemove[i]爲true表示data[i]應該被移除。
要求在O(n)時間複雜度,O(1)空間複雜度,將data中isRemove[i]爲true的元素移除。
算法這種東西,思路我能在腦子裏想象出來畫面,可是說不清楚。。。 大學裏學習算法時候都寫過,就很少說了。
感受寫的有點多。。。以上是和ArrayList
操做密切相關的,其它的簡單總結下吧。
SubList
中。Itr
。基本都是一些包裝代碼。Arrays.sort
實現的,因此,具體的排序算法要看Arrays.sort
。還有一個沒有看懂的問題,即在AbstractList中有一個modCount
字段,ArrayList
的實現中屢次操做該字段。可是仍然沒有理解該字段的做用。
Vector提供的容器模型和ArrayList幾乎如出一轍,只不過它是線程安全的。
它的代碼思路上和ArrayList差很少,可是有一些實現細節上的小區別。
首先,它有一個參數capacityIncrement
,可以控制擴容的細節,看構造函數:
public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } public Vector(int initialCapacity) { this(initialCapacity, 0); }
若是不指定的話,這個initialCapacity
默認值爲0。在內部使用屬性capacityIncrement
保存。
其次,再看控制擴容的核心函數grow
,研究下擴容邏輯是否是有什麼差別:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
能夠發現,capacityIncrement
控制的是擴容的步長。
若是沒有指定步長,那麼則是按照兩倍擴容,這是與ArrayList
不一樣的地方。
整體上,它至關於synchronized同步過的ArrayList,也即它對線程安全的實現很是暴力,並未用到太多的技巧。很顯然,在併發環境下,對vector的操做直接鎖住整個vector,至關於操做vector的線程是串行操做vector,性能不高。