常見的集合框架中,實現的List<E>的主要有Vector和ArrayList以及LinkedList,其中最常見和最常使用的就是ArrayList了
本文的源碼基於jdk1.8
第一問:Vector和ArrayList以及LinkedList區別和聯系,以及分別的應用場景?
1:Vector
Vector的底層的實現其實是一個數組
protected Object[] elementData;
他是線程安全的,為什么呢?
由於經常使用的add()方法的源碼添加synchronized,所以說他是一個同步方法 ,就連不會對數據結構進行修改的get()方法上也加了synchronized
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
如果不手動指定它的容量的話,它默認的容量是10
/** * Constructs an empty vector so that its internal data array * has size {@code 10} and its standard capacity increment is * zero. */ public Vector() { this(10); }
2.LinkedList
LinkedList的底層其實是一個雙向鏈表,每一個對象都是一個Node節點,Node就是一個靜態內部類
private static class Node<E> { //當前節點 E item; //下一個節點 Node<E> next; //上一個節點 Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
它是線程不安全的,所有的方法都沒有加鎖或者進行同步
public boolean add(E e) { linkLast(e); return true; } /** * Links e as last element. */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
3.ArrayList
這里先簡單介紹一下,下面會對ArrayList的擴容機制進行分析
ArrayList是線程不安全的,如果不指定它的初始容量,那么它的初始容量是0,當第一次進行添加操作的時候它的容量將擴容為10
三種集合的使用場景
- Vector很少用,有其他線程安全的List集合
- 如果需要大量的添加和刪除則可以選擇LinkedList 原因是:它查詢的時候需要遍歷整個鏈表,插入和刪除的時候無需移動節點
- 如果需要大量的查詢和修改則可以選擇ArrayList 原因:底層為數組,刪除和插入需要移動其他元素,查詢的時候根據下標來查
第二問:我們想要使用線程安全的List集合,你有什么辦法?
1:可以使用Vector
2.自己重寫類似於ArrayList的但是線程安全的集合
3.可以使用Collections(工具類)中的方法,將ArrayList變成一個線程安全的集合
4.可以使用java.util.concurrent包下的CopyOnWriteArrayList,它是線程安全的
第三問:那你說說CopyOnWriteArrayList是怎么實現線程安全的?
它是juc包下的,專門用於並發編程的,他的設計思想是:讀寫分離,最終一致,寫時復制
它不能指定容量,初始容量是0.它底層也是一個數組,集合有多大,底層數組就有多大,不會有多余的空間
最常使用的add()方法的源碼
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { //獲取一把鎖 final ReentrantLock lock = this.lock; //加鎖 lock.lock(); try { //獲取當前集合(數組) Object[] elements = getArray(); //獲取當前集合的長度 int len = elements.length; //復制一個新的數組,由於是添加操作,新數組的長度比原數組長度大1 Object[] newElements = Arrays.copyOf(elements, len + 1); //原數組的長度就是新數組最大下標,將要添加的元素添加到最后 newElements[len] = e; //更改引用,新數組替代原數組 setArray(newElements); return true; } finally { //釋放鎖 lock.unlock(); } }
remove()方法的實現邏輯也是大同小異,只不過需要移動元素,新數組是減1
/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). Returns the element that was removed from the list. * * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); int numMoved = len - index - 1; if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { lock.unlock(); } }
CopyOnWriteArrayList的缺點
底層是數組,刪除插入的效率不高,寫的時候需要復制,占用內存,浪費空間,如果集合足夠大的時候容易觸發GC
數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。【當執行add或remove操作沒完成時,get獲取的仍然是舊數組的元素】。CopyOnWriteArrayList讀取時不加鎖只是寫入和刪除時加鎖
應用場景:讀操作遠大於寫操作的時候
CopyOnWriteArrayList和Collections.synchronizedList區別
CopyOnWriteArrayList和Collections.synchronizedList是實現線程安全的列表的兩種方式。兩種實現方式分別針對不同情況有不同的性能表現,其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關鍵字的方式,其讀操作性能並不如CopyOnWriteArrayList。因此在不同的應用場景下,應該選擇不同的多線程安全實現類。
第四問:說一下ArrayList的擴容機制
廢話不多說,直接擼源碼,紅色的方法名代表會有解析
無參構造方法
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { //其實就是空數組 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
底層的數組
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access
transient 這個關鍵字的用處是:ArrayList實現了Serializable接口,用transient修飾的字段或者對象不會進行實例化
擴容是再添加元素時才會出現的情況,有的情況是不指定初始容量第一次添加元素時,直接看add()方法
/** * 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; }
跳轉到ensureCapacityInternal方法中進行驗證
private void ensureCapacityInternal(int minCapacity) { //DEFAULTCAPACITY_EMPTY_ELEMENTDATA初始化的值,也就是空 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果是為空的話,默認的DEFAULT_CAPACITY=10傳入的minCapacity哪個大取哪個 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
繼續調用ensureExplicitCapacity方法,傳入判斷之后的值,第一次add的話這個就是默認的10
private void ensureExplicitCapacity(int minCapacity) { //對集合操作的次數 modCount++; // overflow-conscious code //傳入的參數減去數組的長度是否大於0,大於0的話就代表要進行擴容了 if (minCapacity - elementData.length > 0) grow(minCapacity); }
判斷傳入的參數(第一次為10)減去數組的長度是否大於0,大於0的話調用grow擴容方法,數組的長度是elementData.length也可以說是容量,集合的大小是size,兩個值是不同的
/** * 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; //新的容量為舊容量1.5倍,>>1代表右移一位,也就是÷2 int newCapacity = oldCapacity + (oldCapacity >> 1); //新容量-舊容量是否小於0,一般是不指定容量,第一次add時才會進 if (newCapacity - minCapacity < 0) //新容量等於傳入的參數 newCapacity = minCapacity; //如果新的容量超過了集合的閾值 if (newCapacity - MAX_ARRAY_SIZE > 0) //調用hugeCapacity方法進行在一步的計算 newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: //底層的數組進行copy后長度變為新的容量 elementData = Arrays.copyOf(elementData, newCapacity); }
當新容量大於集合的閾值時,調用hugeCapacity方法
private static int hugeCapacity(int minCapacity) { //為負數的話拋出異常,一般沒這個可能 if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //三元表達式:新容量大於集合容量閾值時,新的容量為Integer的最大閾值,否則為集合的閾值 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
MAX_ARRAY_SIZE的值其實為Integer.MAX_VALUE-8,為什么要減8呢?因為數組也是一個對象,對象需要一定的內存存儲對象頭信息,對象頭信息最大占用內存不可超過8字節。
整個ArrayList擴容的機制就如上所示,自己理解的,有不對之處還望指教
第五問:你能自己重寫一個ArrayList嗎?
這個就考驗你對源碼的理解程度,我自己對照着ArrayList寫了一個,就是主要的增刪改查還有擴容,寫的比較拙劣,做個參考嘛
package com.yjc.list; import java.util.AbstractList; import java.util.Arrays; import java.util.List; public class MyList<E> extends AbstractList<E> implements List<E> { //定義無參構造方法 public MyList(){ this.elementData=EMPTY_ELEMENT_DATA; } //定義帶參構造方法 public MyList(int capacity){ //驗證容量是否合法 if (capacity>0) { this.elementData = new Object[capacity]; }else if(capacity==0){ this.elementData=EMPTY_ELEMENT_DATA; }else{ //為負數則拋出異常 throw new IllegalArgumentException("參數"+capacity+"不合法,參數不能為負數"); } } //定義底層數據結構 transient public Object [] elementData; //定義初始化容量 private static final Integer DEFAULT_CAPACITY=10; //集合的最大容量 private static final Integer MAX_CAPACITY=Integer.MAX_VALUE-8; //創建一個空的數組, private static final Object[] EMPTY_ELEMENT_DATA = {}; //用於記錄當前數組的大小 private int size; public boolean add(E e) { ensureCapacityInternal(size+1); //將size+1空間判斷是否夠用 elementData[size++]=e; return true; } //用於判斷數組是否夠用 private void ensureCapacityInternal(Integer capacity){ //代表是第一次添加數據 if (elementData==EMPTY_ELEMENT_DATA){ capacity=DEFAULT_CAPACITY; } if(capacity-elementData.length>0){ //擴容 grow(capacity); } } private void grow(Integer capacity) { //獲取原數組長度 int oldCapacity=elementData.length; //右移兩位,相當於除以2 int newCapacity=oldCapacity+(oldCapacity>>1); //不指定初始大小的時候,第一次執行add方法會走到這 if (newCapacity-capacity<0){ newCapacity=capacity; } //代表超過集合的最大容量 if (newCapacity-MAX_CAPACITY>0){ newCapacity=(capacity>MAX_CAPACITY)?Integer.MAX_VALUE:MAX_CAPACITY; } elementData= Arrays.copyOf(elementData,newCapacity); } @Override public E set(int index, E element) { checkIndex(index); Object oldValue=elementData[index]; elementData[index]=element; //返回舊值 return (E) oldValue; } @Override public E remove(int index) { checkIndex(index); Object oldValue=elementData[index]; size--; for (int i = index; i <size-1; i++) { elementData[i]=elementData[i+1]; } return (E) oldValue; } @Override public void add(int index, E element) { checkIndex(index); ensureCapacityInternal(size+1); for (int i = size+1; i> index; i--) { elementData[i]=elementData[i-1]; } elementData[index]=element; size++; } @Override public int size() { return this.size; } @Override public E get(int index) { checkIndex(index); return (E) elementData[index]; } private void checkIndex(int index){ //驗證下標是否正確 if ((index>=size)||(index<0)){ throw new IndexOutOfBoundsException("輸入的下標不正確,當前集合大小為:"+size); } } }