集合是Java中非常重要而且基礎的內容,平時我們使用得最多,其用法也很簡單,會使用一個,基本其它就很easy了,得益於集合架構的設計,既然第一步使用已經會了,那麼還是有必要深入瞭解一下,學習其設計技巧,理解其本質,這樣不僅會用,還會用得更好,有了更深層次的理解,那麼使用過程中都很明白,而不是亂用一通,如果出現問題,也容易排查,今天我們就開始Java 集合架構的探險之旅。
說道Java集合,估計大家最熟悉的下面的圖了,這個圖是我從網上找的。
這個圖很形象,基本上看這個圖就能大致理清楚Java集合中的關係,先從ArrayList入手。 ArrayList(jdk 1.8)
ArrayList是最常見集合類了,動態數組,顧名思義,ArrayList就是一個以數組形式實現的集合. 繼承體系
ArrayList 繼承 AbstractList,implements List,Serializable,RandomAccess,Cloneable 介面。會發現有趣的現象:AbstractList 是implements了List介面,而ArrayList 也再次implements了List介面。
Random Access List(隨機訪問列表),ArrayList 是隨機訪問的,因此需要打上這個tag,對應的還有Sequence Access List(順序訪問列表)。
Serializable,Cloneable 表明 ArrayList 是可以序列化的,複製的。 資料結構
| 元 素 |
作 用 |
| transient Object[] elementData |
ArrayList是基於數組的一個實現,elementData就是底層的數組 |
| private int size |
ArrayList裡面元素的個數 |
對ArrayList的操作實際就是對數組的操作,只是這個數組可以動態擴張。 初始構造
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_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); } }
也可以指定一個初始大小,那麼ArrayList將產生一個initialCapacity大小的數組。 動態擴張
我們來看看ArrayList是如何擴容的。
/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ 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); }
擴容的時候把元素組大小變為原來的1.5倍。
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); }
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
可以看到,數組擴容很簡單,產生一個和原來類型一致的,長度為newLength的大小的數組,然後將原來數組中的資料拷貝到新的數組中,完成擴容功能。這種我個人覺得我們應該都類似封裝過這種類似的具有動態擴容功能的數組。 添加元素
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
此add是將元素添加至數組末尾,添加前,先檢查是否需要擴容,然後放到數組末尾,O(1)的時間複雜度
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++; }
將元素添加到指定的位置上,首先會檢查索引位置是否有效,這種方式意味著將會移動數組中的資料,將index後的資料依次往後挪一個,然後將資料插入到index位置上,這種添加方式的時間複雜度為O(n),如果index越靠後,那麼效能越好,反之越差。 刪除元素
/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */ 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; }
刪除指定位置上的元素,先把指定元素後面位置的所有元素,利用System.arraycopy方法整體向前移動一個位置,再把最後一個位置的元素指定為null,這樣讓gc可以去回收它。
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; }
按元素刪除,通過remove 方法,我們知道ArrayList裡面是可以存null值的,如果刪除的是null,那麼就會找裡面是null的元素進行刪除,否則將調用對象的equals方法了判斷兩個對象是否相等(根據情況決定是否重寫equals),fastRemove 取名快速刪除,但是裡面還是利用的移動元素方式,其實一點也不快速呀。
其它類似的方法,和添加,刪除差不多,本質上都是對數組的操作,因此這裡就不一一展開了。 複製
ArrayList 實現了 Cloneable 表明這個類是可以複製的,查看Cloneable 發現什麼都沒有,其實Cloneable 我理解的只是一個標籤,表面該類可以複製,至於怎麼複製它不管,此外,在Object中 是有clone方法的,因此Cloneable 沒必要再有clone介面了,我們可以重寫父類clone 方法,按照我們要求進行複製,看看ArrayList 裡面的複製。
/** * Returns a shallow copy of this <tt>ArrayList</tt> instance. (The * elements themselves are not copied.) * * @return a clone of this <tt>ArrayList</tt> instance */ public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
本質上很簡單,就是把底層的數組複製拷貝一遍,這樣就產生了一個新的含有相同資料的ArrayList ,可以看出這種複製方式是淺拷貝,至於深淺拷貝 這裡不會談,原來我在C++中有研究,部落格裡面也有,不過可能寫的品質一般,對於深淺拷貝很簡單,只要理解了基礎資料型別 (Elementary Data Type)和引用性資料類型的區別就明白了。 序列化
ArrayList 實現了 Serializable,說明ArrayList 可以序列化,可以通過網路傳播,但是我們看一下ArrayList中的數組,是這麼定義的:
transient Object[] elementData; // non-private to simplify nested class access
為什麼elementData是使用transient修飾的呢。(被聲明為transient的屬性不會被序列化,這就是transient關鍵字的作用)
Java並不強求使用者非要使用預設的序列化方式,使用者也可以按照自己的喜好自己指定自己想要的序列化方式。
進行序列化、還原序列化時,虛擬機器會首先試圖調用對象裡的writeObject和readObject方法,進行使用者自訂的序列化和還原序列化。如果沒有這樣的方法,那麼預設調用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。換言之,利用自訂的writeObject方法和readObject方法,使用者可以自己控制序列化和還原序列化的過程。
既然elementData 不能被自動序列化,那麼肯定會被手動序列化,那麼我們看看ArrayList 裡面的writeObject方法。
/** * Save the state of the <tt>ArrayList</tt> instance to a stream (that * is, serialize it). * * @serialData The length of the array backing the <tt>ArrayList</tt> * instance is emitted (int), followed by all of its elements * (each an <tt>Object</tt>) in the proper order. */ private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
其實發現簡單,writeObject中只序列化了elementData中有資料的部分,其實這個還是很容易理解的,ArrayList裡面的elementData未必是滿的,那麼是否有必要序列化整個elementData呢。顯然沒有這個必要,因此ArrayList中重寫了writeObject方法。
當然還有對應的還原序列化 readObject方法,這個就是writeObject的逆過程,這裡就不再展開了,可以自己去看一下。 ArrayList與AbstractList
ArrayList重寫了父類的indexOf等方法,在AbstractList中,indexOf方法是用迭代器實現的,在ArrayList中對數組進行順序遍曆實現,重寫的原因就是為了速度。因為ArrayList底層是數組實現的可以隨機訪問,隨機訪問效能比迭代器高。
AbstractList中的indexOf方法:
public int indexOf(Object o) { ListIterator<E> it = listIterator(); if (o==null) { while (it.hasNext()) if (it.next()==null) return it.previousIndex(); } else { while (it.hasNext()) if (o.equals(it.next())) return it.previousIndex(); } return -1; }
ArrayList 中的indexOf方法:
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
總結
從上面的幾個過程總結一下ArrayList的特點:
ArrayList 儲存的資料允許為空白
ArrayList 允許儲存重複資料
ArrayList 儲存資料是有序 (有序的意思是讀取資料的順序和存放資料的順序是否一致)
ArrayList 非安全執行緒(對底層數組的操作未同步)
總結一下ArrayList優缺點。ArrayList的優點如下:
1、ArrayList底層以數組實現,是一種隨機訪問模式,尋找也就是get的時候非常快
2、ArrayList在順序添加一個元素的時候非常方便,只是往數組裡面添加了一個元素而已
ArrayList的缺點:
1、刪除元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費效能
2、插入元素的時候,涉及到一次元素複製,如果要複製的元素很多,那麼就會比較耗費效能
因此,ArrayList比較適合順序添加、隨機訪問的情境。