List與Set
List集合是Collection集合的子介面,其中的元素有序,並且可重複,元素可以通過下標訪問。
接囗特有且常用的方法
1.add(int index, E element)
void add(int index, E element);
將指定的資料element儲存到集合的指定索引index位置上。如果這個索引上存有元素就把原有的元素和隨後的元素向右移動1位(在它們索引上加一),再將儲存element
該方法會進行範圍檢查,如果不在這個範圍的會報:java.lang.IndexOutOfBoundsException
異常
2.get(int index)
E get(int index);
獲取集合中指定索引上的元素
該方法會進行範圍檢查,如果不在這個範圍的會報:java.lang.IndexOutOfBoundsException
異常
3.indexOf(Object o)
int indexOf(Object o);
返回指定元素在這個列表中第一次出現的索引,如果這個列表不包含該元素,則返回-1
4.lastIndexOf(Object o)
int lastIndexOf(Object o);
返回指定元素在這個列表中最後出現的索引,如果這個列表不包含該元素,則返回-1。
5.remove(int index)
E remove(int index);
刪除集合中指定索引處的元素,並將後面的元素向左移動(從它們的索引中減一)
返回被刪除的元素
6.set(int index, E element)
E set(int index, E element);
用指定的元素替換這個列表中指定位置的元素
ArrayList
/** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10; /** * 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
其底層採用了陣列資料結構,其預設容量是10
構造方法
ArrayList有兩種初始化
//面向介面
//預設容量10
List list = new ArrayList();
//自定義集合容量20
Lsit list1 = new ArrayList(20);
使用無參構造,底層會先給賦一個長度為0的陣列,在新增第一個元素時,再把容量設為10
擴容
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倍。elementData會指向這個新陣列,舊陣列會被回收。
如果新容量減去最小容量小於0,則最終擴容的容量為最小容量
Q:建議儘可能減少擴容,因為陣列的擴容效率較低
增刪元素
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++;
}
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;
}
由原始碼可知,ArrayList在向某個索引增加或刪除元素時底層會呼叫
System.arraycopy
,而這一操作極為消耗資源
在涉及到頻繁插入和刪除元素的情況下,LinkedList是首選,因為它只需要新增或刪除節點,並重新連結現有的節點。
優缺點
優點:ArrayList底層是陣列,元素間的記憶體地址是相連的,其檢索效率高,向末尾新增元素效率也高。
而日常工作最常用到的是向末尾新增元素。
缺點:隨機增刪元素效率低,且消費高;陣列不適合儲存大資料,因為記憶體空間沒有很大的一塊地方分配給它
連結串列
連結串列的基本單元是節點:Node
單向連結串列
對於單向連結串列,任意的Node都有兩個屬性,一個是儲存資料,另一個則儲存的是下一節點的記憶體地址
其中最後一個節後儲存記憶體地址的部分指向空值(null)
連結串列中各節點記憶體地址並不連續,要尋找某個元素時,要從頭節點開始查,靠上一節點內部的屬性記錄下一節點的記憶體地址。
隨機增刪元素不會涉及大量節點位移。刪除或增加某個節點時,上一節點要重新記錄下一節點的記憶體地址
雙向連結串列
與單向連結串列相似,不過Node還多了一個儲存上一節點記憶體地址的屬性
在刪除或增加某個節點時,上一節點要重新記錄下一節點的記憶體地址,下一節點也要記錄上一節點的記憶體地址
LinkedList
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
底層採用雙向連結串列儲存資料,first、last分別記錄連結串列的首尾節點。連結串列沒有預設大小,可以不限容量儲存,通過指標關聯
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;
}
}
內部類Node,用於儲存節點資訊
初始化
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
無論使用哪種構造,都會呼叫無參構造
在LinkedList類中,並未對連結串列的容量作說明,而從構造方法可以看出LinkedList是一個無界限的連結串列,可以不限容量儲存
方法
1.新增元素
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
add方法向尾部新增元素。
首先方法內部物件 l 會儲存連結串列尾部節點,然後建立新節點,將新節點的 prev 節點資訊關聯到連結串列當前的尾節點,並把新節點的 next 節點賦值null。再將新創的節點newNode更新為連結串列的尾部節點 last 。再對內部變數 l 做非空引用判斷,如果物件 l 引用為空,則表示當前連結串列中沒有節點,把首節點 first 也更新為newNode;如果物件 l 引用不為空,則把newNode節點與物件 l 指向節點的next屬性關聯。最後再把節點個數加1,集合修改次數加1
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
向指定索引新增節點。
與上面的add方法基本一致,不同的是要讓索引index的上一節點與下一節點重新關聯新的節點
2.刪除節點
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
刪除元素為 o 的節點
要先對連結串列遍歷,因為LinkedList可以儲存null值,所以需要進行null判斷。判斷這個節點是不是首尾節點,讓上一節點的next屬性與 x 節點的next關聯,或讓下一節點的pre屬性與 x 節點的pre關聯,將x的next 、 prev 、 item設為null。因為 x 節點沒有被使用,後面會被垃圾回收器回收
3.更新節點資料
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
更新指定節點儲存的元素
4.是否包含元素
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
判斷元素是否存在於連結串列中
特有方法
1.獲取首或尾節點元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
返回首節點或尾節點儲存的元素
優缺點
因為節點的記憶體地址不連續,隨機增刪元素不會導致大量的元素產生空間位移,隨機增刪元素效率高。但不能通過數學表示式查詢元素的記憶體地址,每次查詢元素都要去遍歷連結串列,檢索效率低
Vector
/**
* The array buffer into which the components of the vector are
* stored. The capacity of the vector is the length of this array buffer,
* and is at least large enough to contain all the vector's elements.
*
* <p>Any array elements following the last element in the Vector are null.
*
* @serial
*/
protected Object[] elementData;
/**
* The number of valid components in this {@code Vector} object.
* Components {@code elementData[0]} through
* {@code elementData[elementCount-1]} are the actual items.
*
* @serial
*/
protected int elementCount;
/**
* The amount by which the capacity of the vector is automatically
* incremented when its size becomes greater than its capacity. If
* the capacity increment is less than or equal to zero, the capacity
* of the vector is doubled each time it needs to grow.
*
* @serial
*/
protected int capacityIncrement;
和ArrayList一樣,其底層也是陣列;elementCount是記錄這個陣列的有效元素個數;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);
}
public Vector() {
this(10);
}
從其底層的構造方法可以看出,預設的陣列長度為10。
擴容
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //10
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 賦值,那其預設的擴容容量是原容量的兩倍
執行緒安全
Vector中所有的方法都有synchronized執行緒同步,是執行緒安全的
在java.util
下還有個Collections
集合工具類,其中有synchronizedList()
方法,可以把List介面下的非執行緒安全集合轉換成執行緒安全的
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
返回一個由指定列表支援的同步(執行緒安全)的列表
當用戶在返回的列表上進行迭代時,必須對其進行手動同步,否則可能會導致非確定性的行為,例:
List list = Collections.synchronizedList(new ArrayList))。
...
synchronized (list) {
Iterator i = list.iterator(); // 必須在同步塊中進行
while (i.hasNext())
foo(i.next())
}
Set
其中的元素無序,元素不可重複
Set介面的方法基本與Collection一致。由於沒有元素無序,Set中沒有索引,所以沒有set和get、index等從索引上訪問元素的方法
與List集合一樣可以通過迭代器或forEach、for迴圈完成遍歷