1. 程式人生 > >「 深入淺出 」集合List

「 深入淺出 」集合List

第一篇文章 「 深入淺出 」java集合Collection和Map 主要講了對集合的整體介紹,本篇文章主要講List相對於Collection新增的一些重要功能以及其重要子類ArrayList、LinkedList、Vector

一、List集合

關於List集合的介紹與方法,可參考第一篇文章 「 深入淺出 」java集合Collection和Map

迭代方法ListIterator

相對於其它集合,List集合添加了一種新的迭代方法ListIterator
ListIterator的方法如下:

image

ListIterator介面在Iterator介面基礎上增加了如下方法:
boolean hasPrevious()

: 如果以逆向遍歷列表。如果迭代器有上一個元素,則返回 true。
E previous():返回迭代器的前一個元素。
void add(Object o):將指定的元素插入列表。
int nextIndex():下一個索引號
int previousIndex():上一個索引號
void set(E e):修改迭代器當前元素的值
void add(E e):在迭代器當前位置插入一個元素

ListIterator介面比Iterator介面多了兩個功能:
1.ListIterator可在遍歷過程中新增和修改
2.ListIterator可逆向遍歷

使用示例如下:

public class ListIteratorDemo {
    public static void main(String[] args) {
        // 建立列表
        List<Integer> list = new ArrayList<Integer>();
        // 向列表中增加10個元素
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        // 獲得ListIterator物件
        ListIterator<Integer> it = list.listIterator();
        // 正序遍歷修改與新增
        while (it.hasNext()) {
            Integer i = it.next();
            //修改元素值
            it.set(i+1);

            if(i == 5 ){
                //新增元素值
                it.add(55);
            }

            //! it.set(i+1);
            // 注意:如果修改的程式碼在這個位置會報錯
            //set操作不能放在add操作之後
            // 這裡不做解析,欲知詳情,請看原始碼
        }

        System.out.println("正向遍歷");
        //正向遍歷
        for(Integer i:list){
            System.out.println(i+" ");
        }

        System.out.println("逆向遍歷");
        //逆向遍歷
        //經過上面迭代器it遍歷後,迭代器it已到達最後一個節點
        while (it.hasPrevious()) {
            System.out.println(it.previous() + " ");
        }
    }
}

二、ArrayList和Vector

ArrayList和Vector很相似,所以就一起介紹了

ArrayList和Vector類都是基於陣列實現的List類,所以ArrayList和Vector類封裝了一個動態的、允許再分配的Object[]陣列。ArrayList和Vector物件使用initalCapacity引數來設定該陣列的長度,當向ArrayList和Vector中新增元素超過了該陣列的長度時,它們的initalCapacity會自動增加。

下面我們通過閱讀JDK 1.8 ArrayList原始碼來了解ArrayList

無參建構函式

預設初始化為容量為10

   /**
     * Constructs an empty list with an initial capacity of ten。
       意思是:構造一個空陣列,預設的容量為10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

有參建構函式

建立指定容量的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);
        }
    }

add方法

執行add方法時,先確保容量足夠大,若容量不夠,則會進行擴容;
擴容大小為原來的1.5倍(這個需要注意一下,面試經常考)

//新增元素e
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    //將對應索引下的元素賦值為e:
    elementData[size++] = e;
    return true;
}
//得到最小擴容量
private void ensureCapacityInternal(int minCapacity) {
    //如果此時ArrayList是空陣列,則將最小擴容大小設定為10:
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //判斷是否需要擴容:
    ensureExplicitCapacity(minCapacity);
}
//判斷是否需要擴容
private void ensureExplicitCapacity(int minCapacity) {
    //運算元+1
    modCount++;
    //判斷最小擴容容量-陣列大小是否大於0:
    if (minCapacity - elementData.length > 0)
        //擴容:
        grow(minCapacity);
}
//ArrayList動態擴容的核心方法:
private void grow(int minCapacity) {
    //獲取現有陣列大小:
    int oldCapacity = elementData.length;
    //位運算,得到新的陣列容量大小,為原有的1.5倍:
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新擴容的大小依舊小於傳入的容量值,那麼將傳入的值設為新容器大小:
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //如果新容器大小,大於ArrayList最大長度:
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //計算出最大容量值:
        newCapacity = hugeCapacity(minCapacity);
    //陣列複製:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
//計算ArrayList最大容量:
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    //如果新的容量大於MAX_ARRAY_SIZE
    //將會呼叫hugeCapacity將int的最大值賦給newCapacity
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

remove方法

有以下兩種刪除方法:

  • remove(int index)是針對於索引來進行刪除,不需要去遍歷整個集合,效率更高;

  • remove(Object o)是針對於物件來進行刪除,需要遍歷整個集合進行equals()方法比對,所以效率較低;

不過,無論是哪種形式的刪除,最終都會呼叫System.arraycopy()方法進行陣列複製操作,等同於移動陣列位置,所以效率都會受到影響

//在ArrayList的移除index位置的元素
public E remove(int index) {
    //檢查索引是否合法:不合法拋異常
    rangeCheck(index);
    //運算元+1:
    modCount++;
    //獲取當前索引的value:
    E oldValue = elementData(index);
    //獲取需要刪除元素 到最後一個元素的長度,也就是刪除元素後,後續元素移動的個數;
    int numMoved = size - index - 1;
    //如果移動元素個數大於0 ,也就是說刪除的不是最後一個元素:
    if (numMoved > 0)
        // 將elementData陣列index+1位置開始拷貝到elementData從index開始的空間
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //size減1,並將最後一個元素置為null
    elementData[--size] = null;
    //返回被刪除的元素:
    return oldValue;
}

//在ArrayList的移除物件為O的元素,不返回被刪除的元素:
public boolean remove(Object o) {
    //如果o==null,則遍歷集合,判斷哪個元素為null:
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //快速刪除,和前面的remove(index)一樣的邏輯
                fastRemove(index);
                return true;
            }
    } else {
        //同理:
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

//快速刪除:
private void fastRemove(int index) {
    //運算元+1
    modCount++;
    //獲取需要刪除元素 到最後一個元素的長度,也就是刪除元素後,後續元素移動的個數;
    int numMoved = size - index - 1;
    //如果移動元素個數大於0 ,也就是說刪除的不是最後一個元素:
    if (numMoved > 0)
        // 將elementData陣列index+1位置開始拷貝到elementData從index開始的空間
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //size減1,並將最後一個元素置為null
    elementData[--size] = null;
}

get方法

通過elementData()方法獲取對應索引元素,在返回時候進行型別轉換

//獲取index位置的元素
public E get(int index) {
    //檢查index是否合法:
    rangeCheck(index);
    //獲取元素:
    return elementData(index);
}
//獲取陣列index位置的元素:返回時型別轉換
E elementData(int index) {
    return (E) elementData[index];
}

set方法

通過elementData獲取舊元素,再設定新元素值相應index位置,最後返回舊元素

//設定index位置的元素值了element,返回該位置的之前的值
public E set(int index, E element) {
    //檢查index是否合法:判斷index是否大於size
    rangeCheck(index);
    //獲取該index原來的元素:
    E oldValue = elementData(index);
    //替換成新的元素:
    elementData[index] = element;
    //返回舊的元素:
    return oldValue;
}

調整容量大小

ArrayList還提供了兩個額外的方法來調整其容量大小

  • void ensureCapacity(int minCapacity): 增加容量,以確保它至少能夠容納最小容量引數所指定的元素數。

  • void trimToSize():將容量調整為列表的當前大小。

Vector實現原理與ArrayList基本相同,可參考上述內容

ArrayList和Vector的主要區別

  • ArrayList是執行緒不安全的,Vector是執行緒安全的。

  • Vector的效能比ArrayList差。

LinkedList

LinkedList是基於雙向連結串列實現的,內部儲存主要是Node物件,該物件儲存著元素值外,還指向上一節點和下一節點。
注意,因為LinkedList是基於連結串列實現的,沒有容量的說法,所以更沒有擴容之說

集合基礎框架

public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

    //LinkedList的元素個數:
    transient int size = 0;

    //LinkedList的頭結點:Node內部類
    transient java.util.LinkedList.Node<E> first;

    //LinkedList尾結點:Node內部類
    transient java.util.LinkedList.Node<E> last;

    //空實現:頭尾結點均為null,連結串列不存在
    public LinkedList() {
    }

    //呼叫新增方法:
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    //節點的資料結構,包含前後節點的引用和當前節點
    private static class Node<E> {
        //結點元素:
        E item;
        //結點後指標
        java.util.LinkedList.Node<E> next;
        //結點前指標
        java.util.LinkedList.Node<E> prev;

        Node(java.util.LinkedList.Node<E> prev, E element, java.util.LinkedList.Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}
LinkedList的常用方法只做簡單介紹不貼原始碼,具體可自行看原始碼

add方法

LinkedList有兩種新增方法

  • add(E e)連結串列最後新增一個元素

  • add(int index, E element)指定位置下新增一個元素;

LinedList新增元素主要分為以下步驟:
1.將新增的元素轉換為LinkedList的Node物件節點;
2.增加該Node節點的前後引用,即該Node節點的prev、next屬性,讓其分別指上、下節點;
3.修改該Node節點的前後Node節點中pre/next屬性,使其指向該節點。

image

remove方法

LinkedList的刪除也提供了2種形式

  • remove(int index)直接通過索引刪除元素

  • remove(Object o)通過物件刪除元素,需要逐個遍歷LinkedList的元素,重複元素只刪除第一個:

刪除後,需要修改上節點的next指向當前下一節點,下節點的prev指向當前上一節點

set方法

set(int index, E element)方法通過node(index)獲取到相應的Node,再修改元素的值

get方法

這是我們最常用的方法,其中核心方法node(int index),需要從頭遍歷或從後遍歷找到相應Node節點
在通過node(int index)獲取到對應節點後,返回節點中的item屬性,該屬性就是我們所儲存的元素。

//獲取相應角標的元素:
public E get(int index) {
    //檢查索引是否正確:
    checkElementIndex(index);
    //獲取索引所屬結點的 元素值:
    return node(index).item;
}
//獲取對應角標所屬於的結點:
java.util.LinkedList.Node<E> node(int index) {
    //位運算:如果位置索引小於列表長度的一半,則從頭開始遍歷;否則,從後開始遍歷;
    if (index < (size >> 1)) {
        java.util.LinkedList.Node<E> x = first;
        //從頭結點開始遍歷:遍歷的長度就是index的長度,獲取對應的index的元素
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        //從集合尾結點遍歷:
        java.util.LinkedList.Node<E> x = last;
        //同樣道理:
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

ArrayList和LinkedList的主要區別

  • ArrayList基於陣列實現的,LinkedList是基於雙向連結串列實現的

  • ArrayList隨機訪問效率高,隨機插入、隨機刪除效率低,需要移動元素位置
    LinkedList隨機插入、隨機刪除效率高,隨機訪問效率低,因需要遍歷連結串列

好叻,搞完,溜了溜了
下一期為<集合Set>,敬請期待

近期推薦:

好人?壞人?做真實的人
「 優質資源 」收藏!最新精選優質資源!
java小心機(5)| 淺談類成員初始化順序

更多精彩內容,可閱讀原文

您的點贊、轉發是對我最大的支援!

image

image

THANDKS

  • End -

一個立志成大腿而每天努力奮鬥的年輕人

伴學習伴成長,成長之路你並不孤單!

掃描二維碼,關注公眾號