《Java集合類1》ArrayList,Vector與Stack

:這三個集合類的底層都是數組實現(Stack繼承自Vector)並且較爲常用。
在這裏插入圖片描述

一般在這幾方面討論集合類:
1底層數據結構
2增刪改查方式
3初始容量,擴容方式,擴容實際
4線程安全與否
5是否允許空,是否允許重複,是否有序

ArrayList

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

每個ArrayList實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。默認初始容量爲10。隨着ArrayList中元素的增加,它的容量也會不斷的自動增長。
在這裏插入圖片描述
在每次添加 add 新的元素時,ArrayList都會檢查是否需要進行擴容操作,擴容操作帶來數據向新數組的重新拷貝,所以如果我們知道具體業務數據量,在構造ArrayList時可以給ArrayList指定一個初始容量,這樣就會減少擴容時數據的拷貝問題。當然在添加大量元素前,應用程序也可以使用ensureCapacity操作來增加ArrayList實例的容量,這可以減少遞增式再分配的數量。
【查看源碼】:
在這裏插入圖片描述
ArrayList 中ensureCapacity方法的使用與優化

package List;
//ArrayList 中ensureCapacity方法的使用與優化

//對於ArrayList 中的一個方法ensureCapacity(int n),這個方法可以對ArrayList底層的
// 數組進行擴容,顯式的調用這個函數。
//  (由源碼可知,系統的默認擴容是:擴容爲原數組長度的1.5倍)
//    如果參數n大於底層數組長度的1.5倍(默認擴容),那麼這個數組的容量就會被擴容到這個參數值n;
//    如果參數n小於底層數組長度的1.5倍(默認擴容),那麼這個容量就會被擴容到底層數組長度的1.5倍。

import java.util.ArrayList;

//總之,就是這個函數可以對底層數組進行擴容,在適當的時機,很好的利用這個函數,
//將會使我們寫出來的程序性能得到提升。
public class ListDemo1 {
    public static void main(String[] args) {
        final int N=1000000;
        Object obj=new Object();
        ArrayList list1=new ArrayList();
        long start=System.currentTimeMillis();
        for(int i=0;i<N;i++){
            list1.add(obj);
        }
        System.out.println(System.currentTimeMillis()-start);

        ArrayList list2=new ArrayList();
        long start2=System.currentTimeMillis();
        list2.ensureCapacity(N);//顯式的對底層數組進行擴容
        for(int i=0;i<N;i++){
            list2.add(obj);
        }
        System.out.println(System.currentTimeMillis()-start2);
    }
}

//運行結果:36
//         17
//第2段的效率顯然比第1段高很多。
// 原因:因爲第1段代碼如果沒有一次性擴到想要的最大容量
// 的話,它就會在添加元素的過程中,一點一點的進行擴容,
//要知道對數組的擴容是要進行數組拷貝的,這就會浪費大量的時間。
// 如果已經預知容器可能會裝多少元素,最好顯式的調用ensureCapacity這個方法,一次性擴容到位。

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

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

2、ArrayList的繼承關係
ArrayList繼承AbstractList抽象父類,實現了List接口(規定了List的操作規範)、RandomAccess(可隨機訪問)、Cloneable(可拷貝)、Serializable(可序列化)。
在這裏插入圖片描述
3、底層數據結構
ArrayList的底層是一個Object數組,並且由transient修飾

   transient Object[] elementData;
                            // non-private to simplify nested class access

被transient修飾,所以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();
//            }
//        }

4、增刪改查

add() :添加元素時,首先判斷索引是否合法,然後檢測是否需要擴容,最後使用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++;
//  }

remove() :刪除元素時,同樣判斷索引是否合法,刪除的方式是把被刪除元素右邊的元素左移,方法同樣是使用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;
//   }

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

set() :修改元素時,只需要檢查下標即可進行修改操作。

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

get() :獲取元素時,也是隻需要檢查下標即可進行獲取操作。

//   public E get(int index) {
//       rangeCheck(index);
//
//       return elementData(index);
//   }

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

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

5、modCount

//        protected transient int modCount = 0;

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

下面是這個的完整解釋 Fail-Fast 機制 因爲 java.util.ArrayList
不是線程安全的,ArrayList,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對ArrayList
內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。 在迭代過程中,判斷
modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 ArrayList。

所以,當我們遍歷那些非線程安全的數據結構時,儘量使用迭代器

6、初始容量and擴容方式

初始容量是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.5、3、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);
    }
}

7、線程安全
ArrayList是線程不安全的。在其迭代器iteator中,如果有多線程操作導致modcount改變,會執行fastfail。拋出異常。

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

Vector

1、概述
Vector可以實現可增長的對象數組。與數組一樣,它包含可以使用整數索引進行訪問的組件。不過,Vector的大小是可以增加或者減小的,以便適應創建Vector後進行添加或者刪除操作。
在這裏插入圖片描述

    Vector實現 List接口,繼承 AbstractList類,所以我們可以將其看做隊列,支持相關的添加、刪除、修改、遍歷等功能。
    Vector實現RandomAccess接口,即提供了隨機訪問功能,提供提供快速訪問功能。在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();
//        }

2、增刪改查
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 addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}
//在指定位置插入**********************************************************
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++;
}

3、初始容量and擴容方式
擴容方式與ArrayList基本一樣,但是擴容時不是1.5倍擴容,而是有一個擴容增量。

 /**
 * Constructs an empty vector with the specified initial capacity and
 * with its capacity increment equal to zero.
 *
 * @param   initialCapacity   the initial capacity of the vector
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

/**
 * Constructs an empty vector so that its internal data array
 * has size {@code 10} and its standard capacity increment is
 * zero.
 */
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);
//        }

4、線程安全
Vector大部分方法都使用了synchronized修飾符,所以他是線程安全的集合類。

Stack

棧是我們常用的數據結構之一。在Java中Stack類表示先進後出(FILo)的對象堆棧。每一個棧都包含一個棧頂,每次出棧是將棧頂的數據取出。

通過繼承了Vector類,Stack類可以很容易的實現他本身的功能。因爲大部分的功能在Vector裏面已經提供支持了。

Stack通過五個操作對Vector進行擴展:這個五個操作如下:

empty():判斷堆棧是否爲空。
peek():查看堆棧頂部的元素,但不取走。
pop():移除堆棧頂部的元素,並作爲函數的值返回該元素。
push(E item):將item壓入棧頂部。
search(Object o):返回元素對象o在堆棧中的位置,以1爲基數。

public class Stack extends Vector
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) {
    // 獲取元素索引,lastIndexOf()具體實現在Vector.java中
    int i = lastIndexOf(o);

    if (i >= 0) {
        return size() - i;
    }
    return -1;
}
   Stack繼承自Vector,所以也是線程安全的

ArrayList和Vector之間的區別

ArrayList的優缺點

   【ArrayList優點】:
           1. ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了
               RandomAccess接口,所以查找get的時候也很快。
           2. ArrayList在順序添加一個元素的時候非常方便,只是往數組中添加
               一個元素而已。

    【ArrayList缺點】:
           1. 刪除元素的時候,涉及到一次元素複製,如果要複製的元素很多,  
               則就會比較耗費性能。
           2. 插入元素的時候,涉及到一次元素複製,如果要複製的元素很多,
               則就會比較耗費性能。
             因此,ArrayList比較適合順序添加、隨機訪問的場景。           

ArrayList和Vector的區別

    ArrayList不是線程安全的,因爲ArrayList中所有的方法都不是同步的,在
    併發下一定會出現線程安全問題。若我們想要使用ArrayList並且讓它線程
    安全那就這樣做:
        第一個方法是使用Collections.synchronizedList方法把我們的ArrayList變成一個線程安全的List,Eg:
public static void main(String[] args) {
    List<String> list=Collections.synchronizedList(new ArrayList<>());
    list.add("aaa");
    list.add("bbb");
    for(int i=0;i<list.size();i++){
        System.out.println(list.get(i));
    
    第二方法是Vector,它是ArrayList的線程安全版本,Vector的實現90%和ArrayList都是一樣的,區別在於:
    Vector是線程安全的,ArrayList不是線程安全的。
    Vector可以指定增長因子,若該增長因子指定了,那麼擴容的時候會使每次新的數組大小會在原數組的大小基礎上加上增長因子;如果不指定增長因子,那麼就給原數組大小*2,源代碼如下:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

—————————————OVER—————_————————————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章