談談集合.List



在Java中,集合框架的使用頻率非常高。在平時開發中,集合常常被用來裝盛其他數據,或者用來實現常見的數據結構比如數組、隊列和棧等。Java中集合主要可以分為Collection和Map兩個大類。Collection又分為List、Queue和Set(見下圖)。本篇博客主要來介紹下List集合。

圖片. Java集合體系

關於List集合,主要掌握ArrayList和LinkedList。同時需要注意是這兩個類都不是線程安全的。


1. ArrayList

ArrayList是開發過程中使用的最多的List接口的實現類。這個類底層用數組實現,可以動態調整數組大小。ArrayList集合中可以盛放任意類型的元素,包括null。這個類還提供了操作數組大小的方法用於store列表。ArrayList和Vector類似,最大的區別就是ArrayList是非線程安全的而Vector是線程安全的。

1.1 ArrayList的構造

創建ArrayList的常見方式有下面兩種

    //創建一個空的數組,當我們向這個ArrayList中添加第一個元素時,會創建一個默認容量為10的數組 
    List<String> strList = new ArrayList<String>();
    //創建指定長度為16的數組 
    List<String> strList2 = new ArrayList<String>(16); 

對於ArrayList的創建,需要提下capacity和size這兩個概念。capacity是指ArrayList底層實現數組的長度,而size是指數組中已經存放的元素的個數。

另外ArrayList還有一個通過Collection創建的構造函數,通過這個構造函數產生的ArrayList中的元素的順序是Collection通過迭代器返回的元素的順序。

public ArrayList(Collection<? extends E> c) {  
        elementData = c.toArray();  
        if ((size = elementData.length) != 0) {  
            // c.toArray might (incorrectly) not return Object[] (see 6260652)  
            if (elementData.getClass() != Object[].class)  
                elementData = Arrays.copyOf(elementData, size, Object[].class);  
        } else { 
            // replace with empty array.  
            this.elementData = EMPTY_ELEMENTDATA;  
        }
 }

在使用創建ArrayList的時候,最好能對ArrayList的大小做一個判斷,這樣有一下幾個好處:

  • 節省內存空間(eg.我們只需要放置兩個元素到數組,new ArrayList (2));
  • 避免數組擴容,引起的效率下降;

1.2 add方法

向ArrayList中添加元素常用的兩個方法是:

  • add(E e);
  • addAll(Collection<? extends E> c);
  • set(int index, E e);
  1. add(E e)方法
public boolean add(E e) {
    ensureCapacity(size + 1);//確保對象數組elementData有足夠的容量,可以將新加入的元素e加進去
    elementData[size++] = e;//加入新元素e,size加1
    return true;
}
//擴容的邏輯
 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);
}

ArrayList的add(E e)的添加邏輯比較簡單,就不把源碼全部貼出來了,大家可以自己去看下。大致的添加過程是:首先判斷當前數組是不是空數組,如果還是空數組,就創建一個長度是10的默認長度的數組,再將元素添加進去;如果當前的ArrayList不是空數組,判斷當前的數組是否已經滿了,如果滿了就進行擴容(擴容的邏輯是oldCapa+oldCapacity/2,如果這個長度還比所需要的最小長度小,就使用所需的最小長度,如果這個最小值大於了數組的最大長度,就是用Integer.MAX_VALUE作為數組長度),再將元素添加進去。在擴容過程中,ArrayList其實是重新創建了一個長度是newCapacity的數組,創建的代碼如下:

//這段代碼效率較高,我們開發過程中可以借鑒使用
elementData = Arrays.copyOf(elementData, newCapacity); 
  1. addAll(Collection<? extends E> c)方法
//將集合c中的元素全部添加到ArrayList的尾部
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

//在ArrayList的指定位置添加元素,同時將ArrayList中其他元素右移
//這個方法在使用時需要特別注意index的范圍
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}
  1. set(int index,E e)
//將下標位置的元素替換成新的元素,並且返回原來位置上的元素
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

1.3 remove方法

常用的刪除方法有如下,這些方法的功能根據方法名很容易就看懂,這邊就不貼源代碼了。

  • public E remove(int index);
  • public boolean remove(Object o);
  • public boolean removeAll(Collection<?> c);
  • protected void removeRange(int fromIndex, int toIndex);
  • public boolean removeIf(Predicate<? super E> filter);
  • public void clear()。

1.4 查詢方法

查詢方法用來查詢ArrayList中是否包含某個元素,常用的查詢方法有如下幾個:

  • public boolean contains(Object o);
  • public int indexOf(Object o);
  • public int lastIndexOf;
  • public E get(int index)。

通過下面的代碼可以看出,判斷相等的標准是兩個元素通過equals方法比較相等。

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    if (o == null) {//返回第一個null的索引
        for (int i = 0; i < size; i++)
            if (elementData[i] == null)
                return i;
    } else {//返回第一個o的索引
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;//若不包含,返回-1
}

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size - 1; i >= 0; i--)
            if (elementData[i] == null)
                return i;
    } else {
        for (int i = size - 1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

1.5 一些其他常用方法

  • public List subList(int fromIndex, int toIndex); //構造子數組,調用這個方法前,做好調用下subListRangeCheck這個方法判斷下參數的正確性
  • public Spliterator spliterator(); //獲得ArrayListSpliterator可分割迭代器,Spliterator是JDK8中添加的可以並行迭代器,可用於多線程迭代,增加效率
  • public void sort(Comparator<? super E> c); //將集合元素根據某種規則排序
  • public Iterator iterator(); //獲取普通的迭代器,也可以通過for循環迭代
  • public Object[] toArray(); //將ArrayList集合中的元素轉換成數組;
  • public T[] toArray(T[] a)

1.6 ArrayList小結

  • ArrayList基於數組方式實現,無容量的限制(最大值是Integer.MAX_VALUE);
  • 添加元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所占的內存空間;
  • 線程不安全;
  • add(int index, E element):添加元素到數組中指定位置的時候,需要將該位置及其后邊所有的元素都整塊向后復制一位;
  • get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1));
  • remove(Object o)需要遍歷數組;
  • remove(int index)不需要遍歷數組,只需判斷index是否符合條件即可,效率比remove(Object o)高;
  • contains(E)需要遍歷數組。

2. Vector

Vector和ArrayList非常相似,底層都是通過數組來實現集合的。Vector和ArrayList最大的區別是Vector的很多方法都是用synchronize修飾的,所以是線程安全的。下面列舉下兩者的主要區別:

  • ArrayList是非線程安全的,Vector是線程安全的;
  • 在創建容器時如果不指定容量,ArrayList會先創建一個空的數組,當第一次添加元素時再將容量擴容到10;Vertor在創建時如果沒指定容量會默認創建一個容量為10的數組;
  • ArrayL在擴容時是1.5倍的擴容,Vector是2倍的擴容;
  • ArrayList支持序列化,Vector不支持。

3. LinkedList

LinkedList的內部維護的是一個雙向鏈表,定義了如下節點(JDK1.8中的代碼,其他版本的會有不同),同時還定義了頭尾指針:

...
transient int size = 0;
transient Node<E> first;
transient Node<E> 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;
    }
}

3.1 LinkedList的構造

 //構造一個空的List
 public LinkedList() {
 }
 //通過Collection集合構造LinkedList
 public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
 }

3.2 鏈表的常見操作

由於LinkedList底層是有鏈表實現的,所以LinkedList提供了很多鏈表的常見操作:

  • private void linkFirst(E e); //頭插法;
  • void linkLast(E e); //尾插法;
  • void linkBefore(E e, Node succ); //在某個節點前插入元素e
  • private E unlinkFirst(Node f); //刪除頭部元素
  • private E unlinkLast(Node l); //刪除尾部元素
  • E unlink(Node x); // 刪除非空元素x

3.3 add操作

  • public boolean add(E e); //在鏈表尾部添加元素;
  • public void add(int index, E element); //在指定位置插入元素element,下標也是從0開始計算的
  • public boolean addAll(Collection<? extends E> c); 添加整個集合
  • public boolean addAll(int index, Collection<? extends E> c); //在指定位置添加集合,下標從0開始計算
  • public E set(int index, E element); //將指定位置的元素設置為element,注意和add的區別,set操作List的size不會增加
public void add(int index, E element) {
    //檢查下標合法性
    checkPositionIndex(index);

    if (index == size)
        //在尾部插入
        linkLast(element);
    else
        //在指定位置插入
        linkBefore(element, node(index));
}

由於底層使用鏈表,所以LinkedList不會有擴容機制。每次add過后,LinkedList的結構如下:

3.4 remove操作

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

//判斷相等的標准也是兩個對象通過equals方法比較相等
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;
}

//清空鏈表
public void clear() {
    // Clearing all of the links between nodes is "unnecessary", but:
    // - helps a generational GC if the discarded nodes inhabit
    //   more than one generation
    // - is sure to free memory even if there is a reachable Iterator
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}

3.5 查詢操作

查詢方法用來查詢LinkedList中是否包含某個元素,常用的查詢方法有如下幾個(和ArrayList中的一致):

  • public boolean contains(Object o);
  • public int indexOf(Object o);
  • public int lastIndexOf;
  • public E get(int index)。
public boolean contains(Object o) {
    return indexOf(o) != -1;
}
//判斷的標准也是equals方法
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;
}

3.6 其他方法

由於LinkedList還實現了Deque這個接口,所以這個類還包含了許多其他方法。這些方法在介紹Queue時在整理。

3.7 LinkedList小結

  • LinkedList基於環形雙向鏈表方式實現,無容量的限制;
  • 添加元素時不用擴容(直接創建新節點,調整插入節點的前后節點的指針屬性的指向即可);
  • 線程不安全;
  • get(int index):需要遍歷鏈表;
  • remove(Object o)需要遍歷鏈表;
  • remove(int index)需要遍歷鏈表;
  • contains(E)需要遍歷鏈表。

4. 線程安全的List

上面提到的ArrayList和LinkedList都是非線程安全的,如果想要得到線程安全的類,可以通過線面的操作進行包裝。

	List list = Collections.synchronizedList(new ArrayList(...));

以上代碼返回的是一個SynchronizedList。這個類和Vector一樣,也是線程安全的。Vector是java.util包中的一個類,而SynchronizedList是java.util.Collections中的一個靜態內部類。這兩個類都是List的子類,而且都是線程安全的。這兩個類有如下的區別:

  • 他們實現同步的方式不一樣,Vector使用同步方法來實現同步,SynchronizedList使用同步代碼塊來實現同步,只是用同步代碼塊將ArrayList的方法封裝了下;
  • 兩個擴容的機制不一樣,Vector擴容成原來的2倍,但是SynchronizedList擴容成原來的1.5倍;
  • SynchronizedList有很好的擴展和兼容功能,他可以將所有的List的子類轉成線程安全的類;
  • SynchronizedList中的方法不全都是同步的,獲取迭代器方法listIterator()就不是同步的,所以在使用迭代器進行遍歷時要手動進行同步處理(或者使用for循環,再調用get方法)。

這邊需要注意的是雖然上述兩個類是線程安全的,但是如果我們在迭代時進行增減元素操作,仍然會有fast-fail異常。也就是說線程安全和快速失敗機制是無關的,快速失敗機制的目的是為了防止在迭代元素的過程中有其他線程改變了當前集合的元素。線程安全是為了解決數據臟讀等問題。

5. 一個注意點

為了將數組轉換為ArrayList,開發者經常會這樣做:

List<String> list = Arrays.asList(arr);

使用Arrays.asList()方法可以得到一個ArrayList,但是得到這個ArrayList其實是定義在Arrays類中的一個私有的靜態內部類。這個類雖然和java.util.ArrayList同名,但是並不是同一個類。java.util.Arrays.ArrayList類中實現了set(), get(), contains()等方法,但是並沒有定義向其中增加元素的方法。也就是說通過Arrays.asList()得到的ArrayList的大小是固定的。

如果在開發過程中,想得到一個真正的ArrayList對象(java.util.ArrayList的實例),可以通過以下方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

java.util.ArrayList中包含一個可以接受集合類型參數的構造函數。因為java.util.Arrays.ArrayList這個內部類繼承了AbstractList類,所以,該類也是Collection的子類。

6. 總結

學東西的最終目的是為了能夠理解、使用它。下面先概括的說明一下各個List的使用場景。如果涉及到可變長度的“數組”,應該首先考慮用List,具體的選擇哪個List,根據下面的標准來取舍。

(01) 對於需要快速插入,刪除元素,應該使用LinkedList。
(02) 對於需要快速隨機訪問元素,應該使用ArrayList。
(03) 對於“單線程環境” 或者 “多線程環境,但List僅僅只會被單個線程操作”,此時應該使用非同步的類(如ArrayList)。對於“多線程環境,且List可能同時被多個線程操作”,此時,應該使用同步的類(如Vector),或者通過Collections工具類將ArrayList和LinkedList包裝成線程安全的類再使用。

7. 參考

公眾號推薦

歡迎大家關注我的微信公眾號「程序員自由之路」


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM