由淺入深理解java集合(三)——集合-List

第一篇文章中介紹了List集合的一些通用知識。本篇文章將集中介紹了List集合相比Collection接口增加的一些重要功能以及List集合的兩個重要子類ArrayList及LinkedList。

一、List集合

關於List集合的介紹及方法,可以參考第一篇文章

List集合判斷元素相等的標準

List判斷兩個對象相等只要通過equals()方法比較返回true即可(關於equals()方法的詳解可以參考第二篇文章中的內容)。
下面以用代碼具體展示。
創建一個Book類,並重寫equals()方法,如果兩個Book對象的name屬性相同,則認爲兩個對象相等。

public class Book {
    public String name;

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Book other = (Book) obj;
        if (this.name == other.name) {
            return true;
        } 
        return false;
    }
}

向List集合中加入book1對象,然後調用remove(Object o)方法,從集合中刪除指定對象,這個時候指定的對象是book2。

public static void main(String[] args){
        Book book1 = new Book();
        book1.name = "Effective Java";
        Book book2 = new Book();
        book2.name = "Effective Java";
        List<Book> list = new ArrayList<Book>();
        list.add(book1);
        list.remove(book2);
        System.out.println(list.size());
    }

輸出結果:

0

可見把book1對象從集合中刪除了,這表明List集合判斷兩個對象相等只要通過equals()方法比較返回true即可。
與Set不同,List還額外提供了一個listIterator()方法,該方法返回一個ListIterator對象。下面具體介紹下ListIterator。

ListIterator

ListIterator接口在Iterator接口基礎上增加了如下方法:

boolean hasPrevious(): 如果以逆向遍歷列表。如果迭代器有上一個元素,則返回 true。
Object previous():返回迭代器的前一個元素。
void add(Object o):將指定的元素插入列表(可選操作)。

與Iterator相比,ListIterator增加了前向迭代的功能,還可以通過add()方法向List集合中添加元素。

二、ArrayList

既然要介紹ArrayList,那麼就順帶一起介紹Vector。因爲二者的用法功能非常相似,可以一起了解比對。

ArrayList簡介

ArrayList和Vector作爲List類的兩個典型實現,完全支持之前介紹的List接口的全部功能。
ArrayList和Vector類都是基於數組實現的List類,所以ArrayList和Vector類封裝了一個動態的、允許再分配的Object[]數組。ArrayList或Vector對象使用initalCapacity參數來設置該數組的長度,當向ArrayList或Vector中添加元素超過了該數組的長度時,它們的initalCapacity會自動增加。下面我們通過閱讀JDK 1.8 ArrayList源碼來了解這些內容。

ArrayList的本質

當以List<Book> list = new ArrayList<Book>(3);方式創建ArrayList集合時,

//動態Object數組,用來保存加入到ArrayList的元素
Object[] elementData;

//ArrayList的構造函數,傳入參數爲數組大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
             //創建一個對應大小的數組對象
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //傳入數字爲0,將elementData 指定爲一個靜態類型的空數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

當以List<Book> list = new ArrayList<Book>();方式創建ArrayList集合時,不指定集合的大小

/**
     *Constructs an empty list with an initial capacity of ten。意思是:構造一個空數組,默認的容量爲10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

在這裏可以看出private static final int DEFAULT_CAPACITY = 10;默認容量確實爲10。
當向數組中添加元素list.add(book1);時:
先調用add(E e)方法

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 數組的大小增加1
        elementData[size++] = e;
        return true;
    }

在該方法中,先調用了一個ensureCapacityInternal()方法,顧名思義:該方法用來確保數組中是否還有足夠容量。
經過一系列方法(不必關心),最後有個判斷:如果剩餘容量足夠存放這個數據,則進行下一步,如果不夠,則需要執行一個重要的方法:

 private void grow(int minCapacity) {
        //......省略部分內容  主要是爲了生成大小合適的newCapacity
       //下面這行就是進行了數組擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

由此,我們就清楚地明白了,ArrayList是一個動態擴展的數組,Vector也同樣如此。
如果開始就知道ArrayList或Vector集合需要保存多少個元素,則可以在創建它們時就指定initalCapacity的大小,這樣可以提高性能。
此外,ArrayList還提供了兩個額外的方法來調整其容量大小:

void ensureCapacity(int minCapacity): 如有必要,增加此 ArrayList 實例的容量,以確保它至少能夠容納最小容量參數所指定的元素數。
void trimToSize():將此 ArrayList 實例的容量調整爲列表的當前大小。

ArrayList和Vector的區別

1.ArrayList是線程不安全的,Vector是線程安全的。
2.Vector的性能比ArrayList差。

Stack

Stack是Vector的子類,用戶模擬“棧”這種數據結構,“棧”通常是指“後進先出”(LIFO)的容器。最後“push”進棧的元素,將被最先“pop”出棧。如下圖所示:

Stack類裏提供瞭如下幾個方法:

Stack與Vector一樣,是線程安全的,但是性能較差,儘量少用Stack類。如果要實現棧”這種數據結構,可以考慮使用LinkedList(下面就會介紹)。

ArrayList的遍歷方式

ArrayList支持3種遍歷方式
(01) 第一種,通過迭代器遍歷

Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}

(02) 第二種,隨機訪問,通過索引值去遍歷
由於ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。

Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
    value = (Integer)list.get(i);        
}

(03) 第三種,for循環遍歷

Integer value = null;
for (Integer integ:list) {
    value = integ;
}

遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,而使用迭代器的效率最低。具體可以測試下。

三、LinkedList

LinkedList簡介

LinkedList類是List接口的實現類——這意味着它是一個List集合,可以根據索引來隨機訪問集合中的元素。除此之外,LinkedList還實現了Deque接口,可以被當作成雙端隊列來使用,因此既可以被當成“棧”來使用,也可以當成隊列來使用。
LinkedList的實現機制與ArrayList完全不同。ArrayList內部是以數組的形式來保存集合中的元素的,因此隨機訪問集合元素時有較好的性能;而LinkedList內部以鏈表的形式來保存集合中的元素,因此隨機訪問集合元素時性能較差,但在插入、刪除元素時性能比較出色。
由於LinkedList雙端隊列的特性,所以新增了一些方法。

LinkedList方法

void addFirst(E e):將指定元素插入此列表的開頭。
void addLast(E e): 將指定元素添加到此列表的結尾。
E getFirst(E e): 返回此列表的第一個元素。
E getLast(E e): 返回此列表的最後一個元素。
boolean offerFirst(E e): 在此列表的開頭插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 獲取但不移除此列表的第一個元素;如果此列表爲空,則返回 null。
E peekLast(E e): 獲取但不移除此列表的最後一個元素;如果此列表爲空,則返回 null。
E pollFirst(E e): 獲取並移除此列表的第一個元素;如果此列表爲空,則返回 null。
E pollLast(E e): 獲取並移除此列表的最後一個元素;如果此列表爲空,則返回 null。
E removeFirst(E e): 移除並返回此列表的第一個元素。
boolean removeFirstOccurrence(Objcet o): 從此列表中移除第一次出現的指定元素(從頭部到尾部遍歷列表時)。
E removeLast(E e): 移除並返回此列表的最後一個元素。
boolean removeLastOccurrence(Objcet o): 從此列表中移除最後一次出現的指定元素(從頭部到尾部遍歷列表時)。

下面我們就以閱讀源碼的方式來了解LinkedList內部是怎樣維護鏈表的。

LinkedList本質

LinkedList調用默認構造函數,創建一個鏈表。由於維護了一個表頭,表尾的Node對象的變量。可以進行後續的添加元素到鏈表中的操作,以及其他刪除,插入等操作。也因此實現了雙向隊列的功能,即可向表頭加入元素,也可以向表尾加入元素

//成員變量:表頭,表尾
 transient Node<E> first;
transient Node<E> last;
//默認構造函數,表示創建一個空鏈表
public LinkedList() {
    }

下面來了解Node類的具體情況

private static class Node<E> {
        //表示集合元素的值
        E item;
       //指向下個元素
        Node<E> next;
     //指向上個元素
        Node<E> prev;
...................................省略
    }

由此可以具體瞭解鏈表是如何串聯起來並且每個節點包含了傳入集合的元素。
下面以增加操作,具體瞭解LinkedList的工作原理。

public boolean add(E e) {
        linkLast(e);
        return true;
    }

調用linkLast(e);方法,默認向表尾節點加入新的元素

 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++;
    }

更新表尾節點,建立連接。其他操作類似,維護了整個鏈表。
下面具體來看,如何將“雙向鏈表和索引值聯繫起來的”?

 public E get(int index) {
        checkElementIndex(index);//檢查索引是否有效
        return node(index).item;
    }

調用了node(index)方法返回了一個Node對象,其中node(index)方法具體如下

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;
        }
    }

首先會比較“index”和“雙向鏈表長度的1/2”;若前者小,則從鏈表頭開始往後查找,直到index位置;否則,從鏈表末尾開始先前查找,直到index位置。這就是“雙線鏈表和索引值聯繫起來”的方法。
到此我們便會明白,LinkedList在插入、刪除元素時性能比較出色,隨機訪問集合元素時性能較差。

LinkedList遍歷方式

LinkedList支持多種遍歷方式。
1.通過迭代器遍歷LinkedList
2通過快速隨機訪問遍歷LinkedList
3.通過for循環遍歷LinkedList
4.通過pollFirst()遍歷LinkedList
5.通過pollLast()遍歷LinkedList
6通過removeFirst()遍歷LinkedList
7.通過removeLast()遍歷LinkedList
實現都比較簡單,就不貼代碼了。
其中採用逐個遍歷的方式,效率比較高。採用隨機訪問的方式去遍歷LinkedList的方式效率最低。

LinkedList也是非線程安全的。

四、ArrayList與LinkedList性能對比

ArrayList 是一個數組隊列,相當於動態數組。它由數組實現,隨機訪問效率高,隨機插入、隨機刪除效率低。ArrayList應使用隨機訪問(即,通過索引序號訪問)遍歷集合元素。
LinkedList 是一個雙向鏈表。它也可以被當作堆棧、隊列或雙端隊列進行操作。LinkedList隨機訪問效率低,但隨機插入、隨機刪除效率高。LinkedList應使用採用逐個遍歷的方式遍歷集合元素。
如果涉及到“動態數組”、“棧”、“隊列”、“鏈表”等結構,應該考慮用List,具體的選擇哪個List,根據下面的標準來取捨。
(01) 對於需要快速插入,刪除元素,應該使用LinkedList。
(02) 對於需要快速隨機訪問元素,應該使用ArrayList。
(03) 對於“單線程環境” 或者 “多線程環境,但List僅僅只會被單個線程操作”,此時應該使用非同步的類(如ArrayList)。對於“多線程環境,且List可能同時被多個線程操作”,此時,應該使用同步的類(如Vector)。

發佈了44 篇原創文章 · 獲贊 54 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章