1. 程式人生 > 其它 >List與Set

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底層是陣列,元素間的記憶體地址是相連的,其檢索效率高,向末尾新增元素效率也高。

而日常工作最常用到的是向末尾新增元素。

缺點:隨機增刪元素效率低,且消費高;陣列不適合儲存大資料,因為記憶體空間沒有很大的一塊地方分配給它

[Data Analytics]

連結串列


連結串列的基本單元是節點: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迴圈完成遍歷