Java集合詳解1:ArrayList,Vector與Stack

本文很是詳盡地介紹了Java中的三個集合類
ArrayList,Vector與Stackhtml

」Java集合詳解系列「是我在完成Java基礎篇的系列博客後準備開始寫的新系列。java

Java集合系列專欄地址:https://blog.csdn.net/column/...git

以前的Java基礎系列博客首發於個人我的博客:https://h2pl.github.io/程序員

在這個分類中,將會寫寫Java中的集合。集合是Java中很是重要並且基礎的內容,由於任何數據必不可少的就是該數據是如何存儲的,集合的做用就是以必定的方式組織、存儲數據。github

之因此把這三個集合類放在一塊兒講解,是由於這三個集合類的底層都是數組實現(Stack繼承自vector)而且比較經常使用。
後面還會另外講底層是鏈表實現的linkedlist和queue;數組

今天咱們來探索一下ArrayList和Vector,以及Stack的源碼安全

具體代碼在個人GitHub中能夠找到微信

https://github.com/h2pl/MyTech網絡

喜歡的話麻煩star一下哈數據結構

ArrayList

ArrayList概述

ArrayList是實現List接口的動態數組,所謂動態就是它的大小是可變的。實現了全部可選列表操做,並容許包括 null 在內的全部元素。除了實現 List 接口外,此類還提供一些方法來操做內部用來存儲列表的數組的大小。

每一個ArrayList實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。默認初始容量爲10。隨着ArrayList中元素的增長,它的容量也會不斷的自動增加。

在每次添加新的元素時,ArrayList都會檢查是否須要進行擴容操做,擴容操做帶來數據向新數組的從新拷貝,因此若是咱們知道具體業務數據量,在構造ArrayList時能夠給ArrayList指定一個初始容量,這樣就會減小擴容時數據的拷貝問題。固然在添加大量元素前,應用程序也可使用ensureCapacity操做來增長ArrayList實例的容量,這能夠減小遞增式再分配的數量。

注意,ArrayList實現不是同步的。若是多個線程同時訪問一個ArrayList實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。因此爲了保證同步,最好的辦法是在建立時完成,以防止意外對列表進行不一樣步的訪問:

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

## 底層數據結構

ArrayList的底層是一個object數組,而且由trasient修飾。

//transient Object[] elementData; //

non-private to simplify nested class access
//ArrayList底層數組不會參與序列化,而是使用另外的序列化方式。

//使用writeobject方法進行序列化,具體爲何這麼作歡迎查看我以前的關於序列化的文章

//總結一下就是隻複製數組中有值的位置,其餘未賦值的位置不進行序列化,能夠節省空間。

//        private void writeObject(java.io.ObjectOutputStream s)
//        throws java.io.IOException{
//            // Write out element count, and any hidden stuff
//            int expectedModCount = modCount;
//            s.defaultWriteObject();
//
//            // Write out size as capacity for behavioural compatibility with clone()
//            s.writeInt(size);
//
//            // Write out all elements in the proper order.
//            for (int i=0; i<size; i++) {
//                s.writeObject(elementData[i]);
//            }
//
//            if (modCount != expectedModCount) {
//                throw new ConcurrentModificationException();
//            }
//        }

增刪改查

//增刪改查

添加元素時,首先判斷索引是否合法,而後檢測是否須要擴容,最後使用System.arraycopy方法來完成數組的複製。

這個方法無非就是使用System.arraycopy()方法將C集合(先準換爲數組)裏面的數據複製到elementData數組中。這裏就稍微介紹下System.arraycopy(),由於下面還將大量用到該方法

。該方法的原型爲:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)。

它的根本目的就是進行數組元素的複製。即從指定源數組中複製一個數組,複製從指定的位置開始,到目標數組的指定位置結束。

將源數組src從srcPos位置開始複製到dest數組中,複製長度爲length,數據從dest的destPos位置開始粘貼。

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

刪除元素時,一樣判斷索引是否和法,刪除的方式是把被刪除元素右邊的元素左移,方法一樣是使用System.arraycopy進行拷貝。

//        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提供一個清空數組的辦法,方法是將全部元素置爲null,這樣就可讓GC自動回收掉沒有被引用的元素了。

//
//        /**
//         * Removes all of the elements from this list.  The list will
//         * be empty after this call returns.
//         */
//        public void clear() {
//            modCount++;
//
//            // clear to let GC do its work
//            for (int i = 0; i < size; i++)
//                elementData[i] = null;
//
//            size = 0;
//        }

修改元素時,只須要檢查下標便可進行修改操做。

//        public E set(int index, E element) {
//            rangeCheck(index);
//
//            E oldValue = elementData(index);
//            elementData[index] = element;
//            return oldValue;
//        }
//
//        public E get(int index) {
//            rangeCheck(index);
//
//            return elementData(index);
//        }
//

上述方法都使用了rangeCheck方法,其實就是簡單地檢查下標而已。

//        private void rangeCheck(int index) {
//            if (index >= size)
//                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//        }

modCount

//        protected transient int modCount = 0;

由以上代碼能夠看出,在一個迭代器初始的時候會賦予它調用這個迭代器的對象的mCount,如何在迭代器遍歷的過程當中,一旦發現這個對象的mcount和迭代器中存儲的mcount不同那就拋異常

好的,下面是這個的完整解釋
Fail-Fast 機制
咱們知道 java.util.ArrayList 不是線程安全的,ArrayList,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。

這一策略在源碼中的實現是經過 modCount 域,modCount 顧名思義就是修改次數,對ArrayList 內容的修改都將增長這個值,那麼在迭代器初始化過程當中會將這個值賦給迭代器的 expectedModCount。

在迭代過程當中,判斷 modCount 跟 expectedModCount 是否相等,若是不相等就表示已經有其餘線程修改了 ArrayList。

因此在這裏和你們建議,當你們遍歷那些非線程安全的數據結構時,儘可能使用迭代器

初始容量和擴容方式

初始容量是10,下面是擴容方法。
首先先取

//        private static final int DEFAULT_CAPACITY = 10;

擴容發生在add元素時,傳入當前元素容量加一
   public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


這裏給出初始化時的數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

這說明:若是數組仍是初始數組,那麼最小的擴容大小就是size+1和初始容量中較大的一個,初始容量爲10。
由於addall方法也會調用該函數,因此此時須要作判斷。
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

//開始精確地擴容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
        若是此時擴容容量大於數組長度嗎,執行grow,不然不執行。
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

真正執行擴容的方法grow

擴容方式是讓新容量等於舊容量的1.5被。

當新容量大於最大數組容量時,執行大數擴容

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

當新容量大於最大數組長度,有兩種狀況,一種是溢出,拋異常,一種是沒溢出,返回整數的最大值。

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

在這裏有一個疑問,爲何每次擴容處理會是1.5倍,而不是2.五、三、4倍呢?經過google查找,發現1.5倍的擴容是最好的倍數。由於一次性擴容太大(例如2.5倍)可能會浪費更多的內存(1.5倍最多浪費33%,而2.5被最多會浪費60%,3.5倍則會浪費71%……)。可是一次性擴容過小,須要屢次對數組從新分配內存,對性能消耗比較嚴重。因此1.5倍剛恰好,既能知足性能需求,也不會形成很大的內存消耗。

處理這個ensureCapacity()這個擴容數組外,ArrayList還給咱們提供了將底層數組的容量調整爲當前列表保存的實際元素的大小的功能。它能夠經過trimToSize()方法來實現。該方法能夠最小化ArrayList實例的存儲量。

public void trimToSize() {
    modCount++;
    int oldCapacity = elementData.length;
    if (size < oldCapacity) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

線程安全

ArrayList是線程不安全的。在其迭代器iteator中,若是有多線程操做致使modcount改變,會執行fastfail。拋出異常。

final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

Vector

Vector簡介

Vector能夠實現可增加的對象數組。與數組同樣,它包含可使用整數索引進行訪問的組件。不過,Vector的大小是能夠增長或者減少的,以便適應建立Vector後進行添加或者刪除操做。

Vector實現List接口,繼承AbstractList類,因此咱們能夠將其看作隊列,支持相關的添加、刪除、修改、遍歷等功能。

Vector實現RandmoAccess接口,即提供了隨機訪問功能,提供提供快速訪問功能。在Vector咱們能夠直接訪問元素。

Vector 實現了Cloneable接口,支持clone()方法,能夠被克隆。

vector底層數組不加transient,序列化時會所有複製

protected Object[] elementData;


//        private void writeObject(java.io.ObjectOutputStream s)
//            throws java.io.IOException {
//            final java.io.ObjectOutputStream.PutField fields = s.putFields();
//            final Object[] data;
//            synchronized (this) {
//                fields.put("capacityIncrement", capacityIncrement);
//                fields.put("elementCount", elementCount);
//                data = elementData.clone();
//            }
//            fields.put("elementData", data);
//            s.writeFields();
//        }

Vector除了iterator外還提供Enumeration枚舉方法,不過如今比較過期。

//        public Enumeration<E> elements() {
//            return new Enumeration<E>() {
//                int count = 0;
//
//                public boolean hasMoreElements() {
//                    return count < elementCount;
//                }
//
//                public E nextElement() {
//                    synchronized (Vector.this) {
//                        if (count < elementCount) {
//                            return elementData(count++);
//                        }
//                    }
//                    throw new NoSuchElementException("Vector Enumeration");
//                }
//            };
//        }
//

增刪改查

vector的增刪改查既提供了本身的實現,也繼承了abstractList抽象類的部分方法。
下面的方法是vector本身實現的。

//
//    public synchronized E elementAt(int index) {
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
//        }
//
//        return elementData(index);
//    }
//
//

//    public synchronized void setElementAt(E obj, int index) {
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " +
//                    elementCount);
//        }
//        elementData[index] = obj;
//    }
//



//    public synchronized void removeElementAt(int index) {
//        modCount++;
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " +
//                    elementCount);
//        }
//        else if (index < 0) {
//            throw new ArrayIndexOutOfBoundsException(index);
//        }
//        int j = elementCount - index - 1;
//        if (j > 0) {
//            System.arraycopy(elementData, index + 1, elementData, index, j);
//        }
//        elementCount--;
//        elementData[elementCount] = null; /* to let gc do its work */
//    }



//    public synchronized void insertElementAt(E obj, int index) {
//        modCount++;
//        if (index > elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index
//                    + " > " + elementCount);
//        }
//        ensureCapacityHelper(elementCount + 1);
//        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
//        elementData[index] = obj;
//        elementCount++;
//    }
//

//    public synchronized void addElement(E obj) {
//        modCount++;
//        ensureCapacityHelper(elementCount + 1);
//        elementData[elementCount++] = obj;
//    }

初始容量和擴容

擴容方式與ArrayList基本同樣,可是擴容時不是1.5倍擴容,而是有一個擴容增量。

//    protected int elementCount;

//    protected int capacityIncrement;
//
//
//    }
//    public Vector() {
//        this(10);
//    }

capacityIncrement:向量的大小大於其容量時,容量自動增長的量。若是在建立Vector時,指定了capacityIncrement的大小;則,每次當Vector中動態數組容量增長時>,增長的大小都是capacityIncrement。若是容量的增量小於等於零,則每次須要增大容量時,向量的容量將增大一倍。

//        public synchronized void ensureCapacity(int minCapacity) {
//            if (minCapacity > 0) {
//                modCount++;
//                ensureCapacityHelper(minCapacity);
//            }
//        }
//        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;
//            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);
//        }

線程安全

vector大部分方法都使用了synchronized修飾符,因此他是線層安全的集合類。

Stack

在Java中Stack類表示後進先出(LIFO)的對象堆棧。棧是一種很是常見的數據結構,它採用典型的先進後出的操做方式完成的。每個棧都包含一個棧頂,每次出棧是將棧頂的數據取出,以下:

image

Stack經過五個操做對Vector進行擴展,容許將向量視爲堆棧。這個五個操做以下:

empty()

測試堆棧是否爲空。

peek()

查看堆棧頂部的對象,但不從堆棧中移除它。

pop()

移除堆棧頂部的對象,並做爲此函數的值返回該對象。

push(E item)

把項壓入堆棧頂部。

search(Object o)

返回對象在堆棧中的位置,以 1 爲基數。

Stack繼承Vector,他對Vector進行了簡單的擴展:

public class Stack<E> extends Vector<E>
Stack的實現很是簡單,僅有一個構造方法,五個實現方法(從Vector繼承而來的方法不算與其中),同時其實現的源碼很是簡單

/**
 * 構造函數
 */
public Stack() {
}

/**
 *  push函數:將元素存入棧頂
 */
public E push(E item) {
    // 將元素存入棧頂。
    // addElement()的實如今Vector.java中
    addElement(item);

    return item;
}

/**
 * pop函數:返回棧頂元素,並將其從棧中刪除
 */
public synchronized E pop() {
    E    obj;
    int    len = size();

    obj = peek();
    // 刪除棧頂元素,removeElementAt()的實如今Vector.java中
    removeElementAt(len - 1);

    return obj;
}

/**
 * peek函數:返回棧頂元素,不執行刪除操做
 */
public synchronized E peek() {
    int    len = size();

    if (len == 0)
        throw new EmptyStackException();
    // 返回棧頂元素,elementAt()具體實如今Vector.java中
    return elementAt(len - 1);
}

/**
 * 棧是否爲空
 */
public boolean empty() {
    return size() == 0;
}

/**
 *  查找「元素o」在棧中的位置:由棧底向棧頂方向數
 */
public synchronized int search(Object o) {
    // 獲取元素索引,elementAt()具體實如今Vector.java中
    int i = lastIndexOf(o);

    if (i >= 0) {
        return size() - i;
    }
    return -1;
}

Stack的源碼不少都是基於Vector,因此這裏再也不累述

區別

ArrayList的優缺點

從上面的幾個過程總結一下ArrayList的優缺點。ArrayList的優勢以下:

一、ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,所以查找也就是get的時候很是快

二、ArrayList在順序添加一個元素的時候很是方便,只是往數組裏面添加了一個元素而已

不過ArrayList的缺點也十分明顯:

一、刪除元素的時候,涉及到一次元素複製,若是要複製的元素不少,那麼就會比較耗費性能

二、插入元素的時候,涉及到一次元素複製,若是要複製的元素不少,那麼就會比較耗費性能

所以,ArrayList比較適合順序添加、隨機訪問的場景。

ArrayList和Vector的區別

ArrayList是線程非安全的,這很明顯,由於ArrayList中全部的方法都不是同步的,在併發下必定會出現線程安全問題。那麼咱們想要使用ArrayList而且讓它線程安全怎麼辦?一個方法是用Collections.synchronizedList方法把你的ArrayList變成一個線程安全的List,好比:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++)
{
    System.out.println(synchronizedList.get(i));
}

另外一個方法就是Vector,它是ArrayList的線程安全版本,其實現90%和ArrayList都徹底同樣,區別在於:

一、Vector是線程安全的,ArrayList是線程非安全的

二、Vector能夠指定增加因子,若是該增加因子指定了,那麼擴容的時候會每次新的數組大小會在原數組的大小基礎上加上增加因子;若是不指定增加因子,那麼就給原數組大小*2,源代碼是這樣的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!(關注公衆號後回覆」資料「便可領取 3T 免費技術學習資源以及我我原創的程序員校招指南、Java學習指南等資源)
在這裏插入圖片描述

相關文章
相關標籤/搜索