文章目錄
ArrayList實現原理
初始化
ArrayList的底層是一個動態數組,初始化時,ArrayList首先會對傳進來的初始化參數initalCapacity進行判斷:
- 如果參數等於0,則將數組初始化爲一個空數組,
- 如果不等於0,將數組初始化爲一個容量爲10的數組。
不過,與HashMap一樣,ArrayList也是一個懶加載的方式。
擴容方式
當數組的大小大於初始容量的時候(比如初始爲10,當添加第11個元素的時候),就會進行擴容,新的容量爲舊的容量的1.5倍。
擴容的時候,會以新的容量建一個原數組的拷貝,修改原數組,指向這個新數組,原數組被拋棄,會被GC回收。
add()方法
對於順序插入,ArrayList特別快,沒什麼好說的。
這裏想說的的是隨機插入,隨機插入的實現方式爲:原來數組插入位置後面的元素按順序複製到原數組插入位置+1的位置。
public void add(int index, E element) {
/*判斷插入的索引是否符合ArrayList範圍,在0 和 size之間,size是ArrayList實際元素個數,不包括底層數組的null元素*/
rangeCheckForAdd(index);
/*擴容機制:判斷添加是否需要進行擴容*/
ensureCapacityInternal(size + 1); // Increments modCount!!
/*將舊數組拷貝到一個新數組中,參數:被複制的原數組, 被複制數組的第幾個元素開始複製, 複製的目標數組, 從目標數組index + 1位置開始粘貼, 複製的元素個數,*/
System.arraycopy(elementData, index, elementData, index + 1, size - index);
/*將新元素賦予該下標*/
elementData[index] = element;
/*元素個數+1*/
size++;
}
實現接口
ArrayList實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸,實現了RandomAccess接口,支持快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable接口,能被克隆。
爲什麼ArrayList裏面的elementData爲什麼要用transient來修飾?
看源碼可以知道,ArrayList包含兩個私有屬性:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
elementData裏面不是所有的元素都有數據,因爲容量的問題,elementData裏面有一些元素是空的,這種是沒有必要序列化的。ArrayList的序列化和反序列化依賴writeObject和readObject方法來實現。可以避免序列化空的元素。
/**
* 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();
}
}
線程安全與否
ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent併發包下的CopyOnWriteArrayList類。
LinkedList實現原理
初始化
Linkedlist底層是一個雙向鏈表,頭尾都包含一個不含任何元素的dummy節點。
//指向頭節點的變量first
transient Node<E> first;
//指向尾節點的變量last
transient Node<E> last;
其初始化方法很簡單(也就是什麼都不用做):
/**
* Constructs an empty list.
*/
public LinkedList() {
}
add()方法
LinkedList包括了頭插和尾插(很容易理解):
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
直接使用add()方法,其實就是調用的尾插:
public boolean add(E e) {
linkLast(e);
return true;
}
而帶index的插入:
public void add(int index, E element) {
//檢查插入位置的合法性
checkPositionIndex(index);
if (index == size)
//如果插入位置在尾部直接使用尾插
linkLast(element);
else
//如果index在前半段就使用頭插,否則使用尾插
linkBefore(element, node(index));
}
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index在前半段就使用頭插,否則使用尾插
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
實現接口
LinkedList 是一個繼承於AbstractSequentialList的雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。
LinkedList 實現了Cloneable接口,即覆蓋了函數clone(),能克隆。
LinkedList 實現java.io.Serializable接口,這意味着LinkedList支持序列化,能通過序列化去傳輸。
線程安全與否
同樣,也不是線程安全的。可以考慮:
- Collections.synchronizedList(new LinkedList<>());
- ConcurrentLinkedQueue
總結:面試如何介紹ArrayList和LinkedList
- 數據結構:
ArrayList底層是數組,採用懶加載的方式初始化,默認長度是10。添加元素如果超過數組長度,採用1.5倍大小進行擴容。
LinkedList底層是雙向鏈表,使頭尾使用不包含有效元素的dummy節點。 - 查詢效率:
由於底層結構的關係,ArrayList實現了隨機訪問,查詢更快。而LinkedList查找某個元素的時間複雜度是O(n)。 - 增刪效率:
ArrayList對於順序增加非常快,但是指定索引的增刪,需要移動索引後面的元素,效率不高。
LinkedList增刪一般來說很快,只需要刪除指定節點並且連同前後兩個節點即可。但是對於指定了索引的刪除,就需要根據索引位置,選擇從頭節點或者尾節點出發找到索引,然後刪除。