List再整理,从代码底层全面解析List(看完后保证收获满满)
前言
本文為對List集合的再一次整理,從父集接口Collection到頂級接口Iterable再到線程不安全實現類:ArrayList、LinkedList,再到線程安全實現類:Vector(被棄用)、CopyOnWriteArrayList。
List
List集合擴展了Collection接口,它是一個允許重復的集合,即允許有多個元素引用相同的對象。
我們來看看List接口的源碼:
public interface List<E> extends Collection<E> {void add(int index,Object ele)// 根據下標添加元素
Boolean addAll(int index,Collection eles) // 在下標index插入另一個集合的全部元素
Object get(index) // 獲取對應下標的元素
int indexOf(Object ele) // ele在集合中首次出現的位置 如果不存在,返回-1
int lastIndexOf(Obj ele) // ele在集合中最后出現的位置 如果不存在,返回-1
Object remove(int index) 移除指定位置索引的元素
Object set(int index,Object ele); // 在下標在下標index插入一個元素}
可以看到List接口繼承了Collection接口,那么接下來看看Collection接口
Collection
在java類庫中,Collection接口是集合類的基本接口,這個接口有兩個基本的方法:
public interface Collection<E> extends Iterable<E>
{boolean add(E element);Iterator<E> iterator();...
}
add方法用于向集合中添加元素。如果添加元素確實改變了集合就返回true,如果集合沒有發生改變就返回false。(其實后面具體的實現類都會對該方法進行重寫)列如,向一個集合中添加一個已經存在的對象,這個添加請求就沒有效果會返回false,因為Set集合不允許重復對象。從代碼中可以看到, Collection接口還包括了一個iterator()方法,返回類型為Iterator接口對象,那么什么Iterator接口?
Collection接口中的其它方法:
//添加方法:
add(Object o) //添加指定元素
addAll(Collection c) //添加指定集合
//刪除方法:
remove(Object o) //刪除指定元素
removeAll(Collection c) //輸出兩個集合的交集
retainAll(Collection c) //保留兩個集合的交集
clear() //清空集合
//查詢方法:
size() //集合中的有效元素個數
toArray() //將集合中的元素轉換成Object類型數組
//判斷方法:
isEmpty() //判斷是否為空
equals(Object o) //判斷是否與指定元素相同
contains(Object o) //判斷是否包含指定元素
containsAll(Collection c) //判斷是否包含指定集合
我們還知道有一個Collections,它是一個包裝類。它包含有各種有關集合操作的靜態多態方法。此類不能實例化,就像一個工具類,服務于Java的Collection框架。
Iterable
翻看Iterable源碼:
public interface Iterable<T> {/*** Returns an iterator over elements of type {@code T}.** @return an Iterator.*/Iterator<T> iterator();
}
可以看到在Iterable接口中定義了一個iterator方法返回類型為Iterator,繼續跟進這個Iterator:
public interface Iterator<E> {boolean hasNext();E next();default void remove() {throw new UnsupportedOperationException("remove");}default void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);while (hasNext())action.accept(next());}
}
很明顯的可以看到,這個Iterator接口就像當于鏈表中的結點,只不過在C語言里結點里的指針在java中變成了對象的引用。那么 我們就應該知道,通過反復的調用nest()就可以逐個的訪問集合中的每個元素,但是當到達了集合的末尾,nest方法將拋出一個NoSuchElementException。因此,每次都用next方法前都應該調用hasNext方法進行判斷。hasNext方法的作用是判斷對象是否還有下一個元素,有就返回true,否則返回false。remove方法會刪除上次調用next方法時返回的元素。就像是刪除一個元素之前先看下它是很有必要的,remove方法就是按照這個理念設計的。舉一個訪問集合中所有元素的案例:
Collection<String> s = new ArrayList<String>();s.add("xiaohong");s.add("xionming");s.add("wanger");Iterator<String> iterator = s.iterator();while (iterator.hasNext()) {String element = iterator.next();System.out.println(element);
在Java SE8版本中,新加入了for each循環遍歷,編譯器簡單地將“for each”循環翻譯為帶有迭代器的循環。
for (String string : s){System.out.println(string);}
顯然,通過"for each"遍歷使得代碼更加簡潔,所有實現了Iterable接口的對象都可以使用"for each"循環。Collection接口擴展了Iterable接口。
好了,到這里我們就看完了List的父級接口了,接下來我們看看它的實現類。
AbstractList
如果實現了Collection接口的每一個類都要實現它的所有方法,那么將是一件很煩的事情。此時,AbstractList應運而生。它將基礎的iterator抽象化,其它的方法給實現了,此時一個具體的集合類就可以擴展AbstractList類,并且只需提供Iterator方法,當然如果不滿意AbstractList類實現的方法也可以在子類重寫它的方法。
線程不安全實現類
ArrayList
我們先來看看ArrayList的源碼:
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...private static final int DEFAULT_CAPACITY = 10;private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData; ...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);}}
}
通過源碼我們可以看到:
- 當通過 new ArrayList()創建對象時,它會分配一個定義好的不能序列化的空的Object數組
- 當通過 new ArrayList(int initialCapacity)創建對象時,當initialCapacity大于0時,會返回一個初始大小為10的Object數組。
- ArrayList 繼承了AbstractList,AbstractList實現了List接口中的大部分方法,提供了相關的添加、刪除、修改、遍歷等功能。
- ArrayList 實現了RandomAccess 接口, RandomAccess 是一個標志接口,表明實現這個這個接口的 List 集合是支持快速隨機訪問的。在 ArrayList 中,我們即可以通過元素的序號快速獲取元素對象,這就是快速隨機訪問。
- ArrayList 實現了Cloneable 接口,即覆蓋了函數 clone(),能被克隆。
- ArrayList 實現java.io.Serializable 接口,這意味著ArrayList支持序列化,能通過序列化去傳輸。
我們再看看ArrayList是如何擴容的,我們跟進add():
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{private int size;private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;...public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}private void grow(int minCapacity) {// overflow-conscious codeint 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) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}
}
分析:
- 首先我們看到它調用了ensureCapacityInternal(size + 1)方法,并傳了一個最小容量過去(minCapacity),其中size初始化大小為0。接著我們跟進ensureCapacityInternal方法;
- 可以看到ensureCapacityInternal方法中又調用了ensureExplicitCapacity()方法,并且ensureExplicitCapacity()方法的參數為calculateCapacity(elementData, minCapacity)的返回值,那我們跟進calculateCapacity(elementData, minCapacity)方法
- 在calculateCapacity中進行了一個判斷,如果數組為null,則返回默認大小10(DEFAULT_CAPACITY),否則返回minCapacity。接著我們再跟進ensureExplicitCapacity
- 可以看到在ensureExplicitCapacity方法中進行了一個判斷,當minCapacity比數組的長度大就調用grow(minCapacity)方法擴容,否則什么都不干,我們跟進grow(minCapacity)
- 可以看到新的容量newCapacity為舊的容量oldCapacity加上oldCapacity右移一位,也就是說新的容量是舊的容量的1.5倍,再將新容量和最小容量進行比較,小于就直接將最小容量付給新的容量。如果新的容量大于MAX_ARRAY_SIZE再調用hugeCapacity函數
- 可以看到最后的擴容會創建一個新的數組,并將老的數組拷貝過來。
LinkedList
LinkedList是一個實現了List接口和Deque接口的雙端鏈表。 LinkedList底層的鏈表結構使它支持高效的插入和刪除操作,另外它實現了Deque接口,使得LinkedList類也具有隊列的特性; LinkedList不是線程安全的,如果想使LinkedList變成線程安全的,可以調用靜態類Collections類中的synchronizedList方法。
需要提一下的是,LinkedList類中有一個ListIterator listIterator方法,listIterator接口中包含一個add方法:
public interface ListIterator<E> extends Iterator<E> {boolean hasNext();E next();boolean hasPrevious();void set(E e);void add(E e);
}
因為鏈表是一個有序的集合,每個對象的位置就顯得十分重要。LinkedList中的add方法只能將對象添加到鏈表尾部,而經常卻要將對象添加到鏈表的中間,迭代器就是用于描述集合中的位置的,所以這種依賴位置的方法就交由迭代器來完成。因為Set集合是無序的,所以在Iterator接口中就沒有add方法,而是擴展了一個LinkIterator接口來實現。
值得一提的是:大家都知道,鏈表是不支持快速隨機訪問的。如果要查看鏈表中的第n個元素,就必須從頭開始,越過n-1個元素,沒有捷徑可走,但盡管如此,LinkedList還是提供了一個用來訪問某個特定元素的get方法,當然這個方法的效率并不高,如果在使用這個方法,那么可能對于所要解決的問題使用了錯誤的數據結構。LinkedList類中get方法所謂的隨機訪問都是需要從列表的頭部開始搜索,效率極低。使用鏈表的唯一理由是盡可能的減少在鏈表中間插入或刪除元素所付出的代價。
LinkedList中特有的方法
//查詢方法:
getFirst() //獲取集合中的第一個元素
getLast() //獲取集合中的最后一個元素
//添加方法:
addFirst(Object o) //在集合的第一個位置添加指定元素
addLast(Object o) //在集合的最后一個位置添加指定元素
//刪除方法:
removeFirst() //刪除集合中的第一個元素
removeLast() //刪除集合中的最后一個元素
下面程序簡單的創建了兩個鏈表,將它們合并在一起,然后從第二個鏈表中每隔一個元素刪除一個元素,最后測試removeAll()方 法 :
package listdemo;import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;public class LinkedListTest {public static void main(String[] args) {List<String> a = new LinkedList<>();a.add("aaa");a.add("bbb");a.add("eee");List<String> b = new LinkedList<>();b.add("AAA");b.add("BBB");b.add("EEE");ListIterator<String> aIter = a.listIterator();ListIterator<String> bIter = b.listIterator();//a集合合并b集合while(bIter.hasNext()) {if(aIter.hasNext())aIter.next();aIter.add(bIter.next());}System.out.println(a);//從b鏈表中每間隔一個元素刪除一個元素while(bIter.hasNext()) {bIter.next();//跳過一個元素if(bIter.hasNext()) {bIter.next();bIter.remove();//先查后刪}}System.out.println(b);//測試刪除所有a.removeAll(a);System.out.println(a);}
}
ArrayList與LinkedList的區別
- 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
- 底層數據結構: Arraylist 底層使用的是 Object 數組;LinkedList 底層使用的是 雙向鏈表 數據結構(JDK1.6之前為循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別)
- 插入和刪除是否受元素位置的影響: ① ArrayList 采用數組存儲,所以插入和刪除元素的時間復雜度受元素位置的影響。 比如:執行add(E e) 方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間復雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element) )時間復雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之后的(n-i)個元素都要執行向后位/向前移一位的操作。 ② LinkedList 采用鏈表存儲,所以插入,刪除元素時間復雜度不受元素位置的影響,都是近似 O(1)而數組為近似 O(n)。
- 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應于get(int index) 方法)。
- 內存空間占用: ArrayList的空間浪費主要體現在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因為要存放直接后繼和直接前驅以及數據)。
ArrayList不安全例子
public class ListTest {public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 0; i < 30; i++) {new Thread(()->{list.add(UUID.randomUUID().toString().substring(1,8));System.out.println(list);},Integer.toString(i)).start();}}
}
運行結果:報java.util.ConcurrentModificationException異常。
導致原因:并發爭搶修改導致
如何解決?有三種方法:
- 使用Collections工具類中的synchronizedList包裝ArrayList方法即:Collections.synchronizedList(new ArrayList<>());其底層實現是根據是否實現RandomAccess接口而new兩個不同的內部類(SynchronizedRandomAccessList<>(list),SynchronizedList<>(list)),然后在添加方法subList中使用了synchronized鎖。
- 使用Vector類(該類因為讀寫方法都用sychronizd關鍵字修飾,性能差,基本已棄用)
- 使用CopyOnWriteArrayList類
線程安全類
Vector
vector類和ArrayList類的差別就是Vector在每個方法前都加了個sychronized鎖,其它地方和ArrayList基本一致,由于性能太差,基本已被棄用。
CopyOnWriteArrayList
CopyOnWrite的意思:在計算機,如果你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之后再將指向原來內存的指針指向新的內存,原來的內存就可以等著被GC回收了。
再來看看源碼:
public class CopyOnWriteArrayList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {final transient ReentrantLock lock = new ReentrantLock();private transient volatile Object[] array;...public CopyOnWriteArrayList() {setArray(new Object[0]);}final void setArray(Object[] a) {array = a;}public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}}public E get(int index) {return get(getArray(), index);}final Object[] getArray() {return array;}
}
通過源碼我們可以看到:
- CopyOnWriteArrayList同樣實現了List, RandomAccess, Cloneable, java.io.Serializable接口。
- 定義了一個可從入鎖lock,在Object數組前加了volatile關鍵字修飾,使得Object線程可見。(關于volatile關鍵字可以看我的另一篇博客:從底層吃透java內存模型(JMM)、volatile、CAS)
- 需要注意的是:CopyOnWriteArrayList只提供了三個構造器:無參構造器、一個參數為List構造器,一個參數為數組構造器。并沒有提供指定初始容量的構造器,這是因為每次的添加操作都是復制一個新的數組來取代舊的數組的,這也就無需指定初始的數組容量大小了
- CopyOnWriteArrayList 類的所有可變操作(add,set等等)都是通過創建底層數組的新副本來實現的。當 List 需要被修改的時候,并不修改原有內容,而是對原有數據進行一次復制,將修改的內容寫入副本。寫完之后,再將修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀操作了。
- 在所有的讀取操作時是不加鎖的,所以CopyOnWriteArrayList適用于讀多寫少的場景中,事實上就算是寫多的場景CopyOnWriteArrayList在性能上也要好于Vector,所以這就導致著Vector基本被棄用。
最后附上在最開始說的ArrayList線程不安全使用CopyOnWriteArrayList解決的代碼:
public class CopyOnWriteArrayListTest {public static void main(String[] args) {List<String> list = new CopyOnWriteArrayList<>();for (int i = 0; i < 30; i++) {new Thread(()->{list.add(UUID.randomUUID().toString().substring(1,8));System.out.println(list);},Integer.toString(i)).start();}}
}
代碼
本文所涉及的所有代碼都在我的GitHub上:https://github.com/dave0824/jmm
推薦閱讀
從底層吃透java內存模型(JMM)、volatile、CAS
總結
以上是生活随笔為你收集整理的List再整理,从代码底层全面解析List(看完后保证收获满满)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Conversion error:Jek
- 下一篇: Map再整理,从底层源码探究HashMa