Java集合(二):List列表

來源:互聯網
上載者:User

標籤:asn   return   編譯   1.4   img   ons   tor   結構   程式   

在上一節中,介紹了Java集合的總體情況。從這節開始,將介紹詳細的類。這裡不單單介紹類的使用方法。還會試圖從原始碼的角度分析類的實現。這一節將介紹List介面及實作類別。即列表中的鏈表LinkedList和數組列表ArrayList。

1 List介面及抽象類別

List介面擴充自Collection介面,這個介面設計了一些適合列表操作的方法。List是一個有序集合。元素能夠加入到容器中某個特定的位置。

使用javac編譯List.java原始碼後,能夠使用javap反編譯原始碼獲得介面的詳細資料。例如以下是調用後的結果:

Compiled from "List.java"public interface java.util.List<E> extends java.util.Collection<E> {  public abstract int size();  public abstract boolean isEmpty();  public abstract boolean contains(java.lang.Object);  public abstract java.util.Iterator<E> iterator();  public abstract java.lang.Object[] toArray();  public abstract <T> T[] toArray(T[]);  public abstract boolean add(E);  public abstract boolean remove(java.lang.Object);  public abstract boolean containsAll(java.util.Collection<?>);  public abstract boolean addAll(java.util.Collection<? extends E>);  public abstract boolean addAll(int, java.util.Collection<? extends E>);  public abstract boolean removeAll(java.util.Collection<?>);  public abstract boolean retainAll(java.util.Collection<?>);  public void replaceAll(java.util.function.UnaryOperator<E>);  public void sort(java.util.Comparator<? super E>);  public abstract void clear();  public abstract boolean equals(java.lang.Object);  public abstract int hashCode();  public abstract E get(int);  public abstract E set(int, E);  public abstract void add(int, E);  public abstract E remove(int);  public abstract int indexOf(java.lang.Object);  public abstract int lastIndexOf(java.lang.Object);  public abstract java.util.ListIterator<E> listIterator();  public abstract java.util.ListIterator<E> listIterator(int);  public abstract java.util.List<E> subList(int, int);  public java.util.Spliterator<E> spliterator();}
List介面提供了這些方法,大部分是Abstract的,但也有一部分不是,這部分方法是JDK 1.8 新增的default方法。比方sort方法。

List介面提供了隨機訪問方法,比方get(int)方法,可是List並無論這些方法都某個特定的實現是否高效。

為了避免運行成本較高的隨機訪問操作,Java SE 1.4 引入了一個標記介面RandomAccess。

這個介面沒有不論什麼方法,但能夠用來檢測一個特定的集合是否支援高效的隨機訪問:

if(c instanceof RandomAccess){    use random access algorighm}else{    use sequential access algorithm}
ArrayList就實現了這個介面。

List介面中的例行方法在抽象類別AbstractList中實現了,這樣就不須要在詳細的類中實現,比方isEmpty方法和contains方法等。

這些例行方法比較簡單。含義也明顯。對於隨機訪問元素的類(比方ArrayList),優先繼承這個抽象類別。

在AbstractList抽象類別中。有一個重要的域。叫modCount:

protected transient int modCount = 0;

這個域能夠用來跟蹤列表結構性改動的次數,什麼是結構性改動呢?就是改變列表長度的改動,比方添加、刪除等。

對於僅僅改動某個節點的值不算結構性改動。

這個域在後面的迭代器中很實用。

迭代器能夠使用這個域來檢測並發改動問題,這個問題會在LinkedList類中介紹。

抽象類別AbstractSequentialList實現了List介面中的一些方法,對於順序訪問元素的類(比方LinkedList),優先繼承這個抽象類別。

2 鏈表:LinkedList

鏈表是一個大家很熟悉的資料結構。

鏈表攻克了數組列表插入和刪除元素效率太低的問題,鏈表的插入和刪除就很高效。

鏈表將每一個對象存放在獨立的節點中。

Java中的LinkedList鏈表,每一個節點除了有後序節點的引用外,另一個前序節點的引用,也就是說。LinkedList是一個雙向鏈表。

LinkedList類有三個域,各自是大小、頭結點和尾節點:

transient int size;transient Node<E> first;transient Node<E> last;

還有兩個構造器,一個無參構造器和一個含參構造器:

public java.util.LinkedList();public java.util.LinkedList(java.util.Collection<? extends E>);

當中無參構造器構造一個空的鏈表,含參構造器依據傳進來的一個集合構造一個鏈表。

2.1 Node<E>內部類

LinkedList類中,定義了一個Node<E>內部類來表示一個節點。

這個類的定義例如以下:

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;    }}
這是一個靜態內部類,也沒有對外部的引用。這個類有三個域:值。前序節點的引用,後序節點的引用,也有一個構造方法。定義非常easy。

假設要建立一個Node節點,能夠這樣:

Node<E> node=new Node<>(pre,item,next);

當中,pre和next各自是前序節點和後序節點的引用。

2.2 鏈表操作的基本方法

既然是鏈表。就少不了鏈表節點的加入與刪除。

在LinkedList類中,提供了六個主要的鏈表操作的方法。這些方法都對鏈表的結構進行改動,因此會改變AbstractList類中的modCount域,這六個方法例如以下:

private void linkFirst(E);//在鏈表頭部加入給定值的節點作為頭結點void linkLast(E);//在鏈表尾部加入一個給定值的節點作為尾節點void linkBefore(E, java.util.LinkedList$Node<E>);//在給定的節點前插入一個節點private E unlinkFirst(java.util.LinkedList$Node<E>);//刪除頭結點。並返回頭結點的值private E unlinkLast(java.util.LinkedList$Node<E>);//刪除尾節點,並返回尾節點的值E unlink(java.util.LinkedList$Node<E>);//刪除給定的節點
這些方法都是私人的(或包內私人的)。因此能夠稱為工具方法,LinkedList類中的全部結構性改動操作都是基於這六個方法實現的。

這六個方法都是鏈表的基本操作。代碼比較簡單。只是給出實現能夠看看原始碼實現者的寫法,對於自己編程還是有協助的:

    /**     * 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++;    }    /**     * Links e as last element.     */    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++;    }    /**     * Inserts element e before non-null Node succ.     */    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++;    }    /**     * Unlinks non-null first node f.     */    private E unlinkFirst(Node<E> f) {        // assert f == first && f != null;        final E element = f.item;        final Node<E> next = f.next;        f.item = null;        f.next = null; // help GC        first = next;        if (next == null)            last = null;        else            next.prev = null;        size--;        modCount++;        return element;    }    /**     * Unlinks non-null last node l.     */    private E unlinkLast(Node<E> l) {        // assert l == last && l != null;        final E element = l.item;        final Node<E> prev = l.prev;        l.item = null;        l.prev = null; // help GC        last = prev;        if (prev == null)            first = null;        else            prev.next = null;        size--;        modCount++;        return element;    }    /**     * Unlinks non-null node x.     */    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;    }
2.3 列表迭代器:ListIterator介面

鏈表是一個有序集合。每一個對象的位置十分重要。LinkedList.add方法僅僅是將節點加到尾部,然而對於鏈表的操作還有非常大一部分須要將節點加入到鏈表中間。

因為迭代器是秒數集合中的位置的。所以這樣的依賴位置的加入方法將由迭代器負責。

僅僅有對自然有序的集合使用迭代器加入元素才有意義。

比方,對於無序的集合set,在Iterator介面中就沒有add方法。相反的,在集合類庫中提供了ListIterator介面,當中就有add方法:

interface ListIterator<E> extends Iterator<E>{    void add(E element);    ...}
與Collection介面中的add方法不同,這種方法不返回boolean類型的值。由於它假定加入操作總是改變鏈表。

另外。除了hasNext和next方法,ListIterator介面還提供了以下的兩個方法:

E previous();boolean hasPrevious();
這兩個方法用來反向遍曆鏈表。previous也像next一樣,返回越過的對象。

LinkedList類的listIterator方法返回一個迭代器對象:

ListIterator<String> iter=list.listIterator();
在介紹介面時我們知道。不能執行個體化一個介面對象。但能夠聲明一個介面對象然後引用一個實現了該介面的類的執行個體。那麼listIterator方法返回的就必定是一個類的執行個體,而這個類也必定實現了這個介面,問題是。這個類是什嗎?

這個類事實上是LinkedList的一個內部類,即ListItr:

Compiled from "LinkedList.java"class java.util.LinkedList$ListItr implements java.util.ListIterator<E> {  private java.util.LinkedList$Node<E> lastReturned;  private java.util.LinkedList$Node<E> next;  private int nextIndex;  private int expectedModCount;  final java.util.LinkedList this$0;  java.util.LinkedList$ListItr(java.util.LinkedList, int);  public boolean hasNext();  public E next();  public boolean hasPrevious();  public E previous();  public int nextIndex();  public int previousIndex();  public void remove();  public void set(E);  public void add(E);  public void forEachRemaining(java.util.function.Consumer<? super E>);  final void checkForComodification();}
上面也是使用javap反編譯的結果。能夠看到。這個內部類實現了ListIterator介面,並實現了這個介面的方法。

這正是理解迭代器的關鍵。我們知道,迭代器能夠看做是一個位置。這個位置在兩個節點的中間,也就是說,對於一個大小為n的鏈表,迭代器的位置有n+1個:

| a | b | ...| z |

在這個範例中。鏈表表示26個字母,迭代器的位置就有27個。

這裡也是把迭代器形象化為游標。next方法就是游標移到下一個位置,飯後返回剛剛越過的元素,同理previous也是一樣。僅僅只是是左移一個位置。然後返回剛剛越過的元素。以下是這兩個方法的代碼:

public E next() {    checkForComodification();    if (!hasNext())        throw new NoSuchElementException();    lastReturned = next;    next = next.next;    nextIndex++;    return lastReturned.item;}public E previous() {    checkForComodification();    if (!hasPrevious())        throw new NoSuchElementException();    lastReturned = next = (next == null) ? last : next.prev;    nextIndex--;    return lastReturned.item;}
這兩個方法首先調用checkForComodifcation方法檢查並發改動問題。前面說過,AbstractList的modCount記錄了鏈表的改動次數,而每個迭代器都通過以下的欄位維護一個獨立的計數器:

private int expectedModCount = modCount;
這個域初始化為類的modCount改動次數。而checkForComodification檢查迭代器自己維護的計數器是否和類的modCount相等,假設不等。就會拋出一個ConcurrentModificationException。

並發改動檢查通過後。會調用hasNext或hasPrevious方法檢查是否有待訪問的元素。ListItr類有一個nextIndex域:

private int nextIndex;
這個域維護迭代器的當前位置。當然,對於LinkedList來說,因為迭代器指向兩個元素中間,所以能夠同一時候產生兩個索引:nextIndex方法返回下一次調用next方法時返回元素的整數索引。previousIndex返回下一次調用previous方法時返回元素的索引,這個索引比nextIndex小1。

hasNext和hasPrevious方法就是檢查nextIndex和previousIndex是否在正確範圍來確實是否有待訪問元素的。

ListItr類還有兩個域:

private Node<E> lastReturned;private Node<E> next;
lastReturned用來儲存上次返回的節點,next就是迭代器位置的下一個元素。也能夠看做游標的下一個元素(下一個元素總是游標的右面那個元素)。調用next方法後,游標右移一位。越過next域儲存的節點,然後更新這兩個域的值,即剛才的next變為lastReturned,next就是再下一個元素,然後nextIndex增1。

previous相對於next操作來說相當於游標左移一位,在更新lastReturned和next時,須要考慮next是否為null。假設next為null,說明在沒運行previous時。迭代器在最後一個位置,所以運行previous後。next應該是鏈表的尾節點last。假設next不是null,那麼next更新為next的前序節點。而lastReturned為游標剛越過的元素。即如今的next節點,這時,lastReturned和next節點指向同一個元素。

ListItr類有三個能夠改動鏈表的方法:add、remove和set。當中add和remove會改變迭代器的位置。由於這兩個方法改動了鏈表的結構;而set方法不會改動迭代器的位置。由於它不改動鏈表的結構。

這三個方法的代碼例如以下:

public void remove() {    checkForComodification();    if (lastReturned == null)        throw new IllegalStateException();    Node<E> lastNext = lastReturned.next;    unlink(lastReturned);    if (next == lastReturned)        next = lastNext;    else        nextIndex--;    lastReturned = null;    expectedModCount++;}public void set(E e) {    if (lastReturned == null)        throw new IllegalStateException();    checkForComodification();    lastReturned.item = e;}public void add(E e) {    checkForComodification();    lastReturned = null;    if (next == null)        linkLast(e);    else        linkBefore(e, next);    nextIndex++;    expectedModCount++;}
值得注意的是remove方法。

在每次調用remove方法後,都會將lastReturned置為null。也就是說。假設連續調用remove方法,第二次調用就會拋出一個IllegalStateException異常。

因此。remove操作必須跟在next或previous操作之後。

如今已經介紹了ListIterator介面的基本方法,能夠從前後兩個方向遍曆鏈表中的元素,並能夠加入、刪除元素。

記住一點:鏈表的任何位置加入與刪除節點的操作是ListIterator迭代器提供的,類本身的add方法僅僅能在結尾加入。

2.4 隨機訪問

在Java類庫中,還提供了很多理論上存在一定爭議的方法。鏈表不支援高速隨機訪問。假設要查看鏈表中的第n個元素,就必須從頭開始。越過n-1個元素,沒有捷徑可走。鑒於這個原因。在程式須要採用整數索引訪問元素時。一般不選用鏈表。

雖然如此,LinkedList類還提供了一個用來訪問某個特定元素的get方法:

LinkedList<String> list=...;String s=list.get(n);
當然,這種方法的效率不太高。

絕不應該使用這樣的讓人誤解的隨機訪問方法來遍曆鏈表。以下的代碼效率極低:

for(int i=0;i<list.size();i++){    dosomething with list.get(i);}

每次尋找一個元素都要從頭開始又一次搜尋。LinkedList對象根本不做不論什麼緩衝位置資訊的處理。

事實上。在LinkedList類中,get方法會推斷當前的位置距離頭和尾哪一端更近,然後推斷從左向右遍曆還是從右向左遍曆。

2.5 範例

以下的代碼示範了LinkedList類的基本操作。

它簡單的建立兩個鏈表,將它們合并在一起,然後從第二個鏈表中每間隔一個元素刪除一個元素。最後測試removeAll方法:

import java.util.*;public class LinkedListTest {    public static void main(String[] args) {        List<String> a=new LinkedList<>();        a.add("A");        a.add("C");        a.add("E");        List<String> b=new LinkedList<>();        b.add("B");        b.add("D");        b.add("F");        b.add("G");        ListIterator<String> aIter=a.listIterator();        Iterator<String> bIter=b.iterator();        while(bIter.hasNext()){            if(aIter.hasNext())aIter.next();            aIter.add(bIter.next());        }        System.out.println(a);        bIter=b.iterator();        while(bIter.hasNext()){            bIter.next();            if(bIter.hasNext()){                bIter.next();                bIter.remove();            }        }        System.out.println(b);        a.removeAll(b);        System.out.println(a);    }}
結果例如以下:


3 數組列表:ArrayList

前面介紹了List介面和實現了這個介面的LinkedList類。List介面用於描寫敘述一個有序集合,而且集合中每一個元素的位置十分重要。

有兩種訪問元素的協議:一種是用迭代器。還有一種使用get和set方法隨機訪問每一個元素。

後者不適用於鏈表,但對數組非常實用。集合類庫提供了一個大家非常熟悉的ArrayList類,這個類也實現了List介面。ArrayList類封裝了一個動態再分配的對象數組。

Java集合類庫中另一個動態數組:Vector類。只是這個類的全部方法是同步的,能夠由兩個安全執行緒的訪問一個Vector對象。

可是,假設一個線程訪問Vector。代碼要在同步上消耗大量的時間。

而ArrayList方法不是同步的,因此。假設不須要同步時使用ArrayList。

在ArrayList具體解釋中具體介紹了類的實現及方法的使用。

Java集合(二):List列表

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.