Java List 容器原始碼分析的補充

像一隻狗發表於2018-04-03

Java List 容器原始碼分析的補充

之前我們通過分析原始碼的方式學習了 ArrayList 以及 LinkedList 的使用方法。但是在分析原始碼之餘,總免不了去網上查詢一些相關資料,站在前人的肩膀上,發現前兩篇文章多多少少有些遺漏的地方,比如跟 ArrayList 很相似的 Vector 還沒有提及過,所以本文想從面試中對於 List 相關問題出發,來填一填之前的坑,並對 List 家族中的實現類成員的異同點試著做出總結。

  1. Vector 介紹及與 ArrayList 的區別
  2. ArrayList 與 LinkedList 的區別
  3. Stack 類的介紹及實現一個簡單的 Stack
  4. SynchronizedList 與 Vector的區別

Java List 容器原始碼分析的補充

Vector 介紹

Vector 是一個相當古老的 Java 容器類,始於 JDK 1.0,並在 JDK 1.2 時代對其進行修改,使其實現了 ListCollection 。從作用上來看,VectorArrayList 很相似,都是內部維護了一個可以動態變換長度的陣列。但是他們的擴容機制卻不相同。對於 Vector 的原始碼大部分都和 ArrayList 差不多,這裡簡單看下 Vector 的建構函式,以及 Vector 的擴容機制。

Vector 的建構函式可以指定內部陣列的初始容量和擴容係數,如果不指定初始容量預設初始容量為 10,但是不同於 ArrayList 的是它在建立的時候就分配了容量為10的記憶體空間,而 ArrayList 則是在第一次呼叫 add 的時候才生成一個容量為 10 陣列。

public Vector() {
   this(10);//建立一個容量為 10 的陣列。
}
    
public Vector(int initialCapacity) {
   this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
   super();
   if (initialCapacity < 0)
       throw new IllegalArgumentException("Illegal Capacity: "+
                                          initialCapacity);
   this.elementData = new Object[initialCapacity];
   this.capacityIncrement = capacityIncrement;
}

// 此方法在 JDK 1.2 後新增
public Vector(Collection<? extends E> c) {
   elementData = c.toArray();//建立與引數集合長度相同的陣列
   elementCount = elementData.length;
   // c.toArray might (incorrectly) not return Object[] (see 6260652)
   if (elementData.getClass() != Object[].class)
       elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}

複製程式碼

對於 Vector的擴容機制,我們只需要看下內部的 grow 方法原始碼:

private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;
   // 如果我們沒有指定擴容係數,那麼 newCapacity = 2 * oldCapacity 
   // 如果我們指定了擴容係數,那麼每次增加指定的容量
   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 的需要擴容的時候,首先會判斷 capacityIncrement 即在構造的 Vector 的時候時候指定了擴容係數,如果指定了則按照指定的係數來擴大容量,擴大後新的容量為 oldCapacity + capacityIncrement,如果沒有指定capacityIncrement的大小,則預設擴大原來容量的一倍,這點不同於 ArrayList 的 0.5 倍長度。

對於 VectorArrayList 的區別最重要的一點是 Vector所有的訪問內部陣列的方法都帶有synchronized ,這意味著 Vector 是執行緒安全的,而ArrayList 並沒有這樣的特性。

對於 Vector 而言,除了 for 迴圈,高階 for 迴圈,迭代的迭代方法外,還可以呼叫 elements() 返回一個 Enumeration

Enumeration 是一個介面,其內部只有兩個方法hasMoreElementsnextElement,看上去和迭代器很相似,但是並沒迭代器的 add remove,只能作用於遍歷。

public interface Enumeration<E> {
    boolean hasMoreElements();
    E nextElement();
}
// Vector 的 elements 方法。
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<String> vector = new Vector<>();
vector.add("1");
vector.add("2");
vector.add("3");

Enumeration<String> elements = vector.elements();

while (elements.hasMoreElements()){
  System.out.print(elements.nextElement() + " ");
}
複製程式碼

事實上,這個介面也是很古老的一個介面,JDK 為了適配老版本,我們可以呼叫類似 Enumeration<String> enumeration = Collections.enumeration(list); 來返回一個Enumeration 。其原理就是呼叫對應的迭代器的方法。

// Collections.enumeration 方法
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
   return new Enumeration<T>() {
        // 構造對應的集合的迭代器
       private final Iterator<T> i = c.iterator();
        // 呼叫迭代器的 hasNext
       public boolean hasMoreElements() {
           return i.hasNext();
       }
       // 呼叫迭代器的 next
       public T nextElement() {
           return i.next();
       }
   };
}
複製程式碼

Vector 與 ArrayList 的比較

  1. VectorArrayList 底層都是陣列資料結構,都維護著一個動態長度的陣列。
  2. Vector 對擴容機制在沒有通過構造指定擴大系數的時候,預設增長現有陣列長度的一倍。而 ArrayList 則是擴大現有陣列長度的一半長度。
  3. Vector 是執行緒安全的, 而 ArrayList 不是執行緒安全的,在不涉及多執行緒操作的時候 ArrayList 要比 Vector 效率高
  4. 對於 Vector 而言,除了 for 迴圈,高階 for 迴圈,迭代器的迭代方法外,還可以呼叫 elements() 返回一個 Enumeration 來遍歷內部元素。

ArrayList 與 LinkedList 的區別

我們先回過頭來看下,這兩個 List 的繼承體系有什麼不同:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製程式碼
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
複製程式碼

可以看出 LinkedList 沒有實現 RandomAccess 介面,我們知道RandomAccess 是一個空的標記介面,標誌著實現類具有隨機快速訪問的特點。那麼我們有必要重新認識下這個介面,根據 RandomAccess 的 Java API 說明:

公共介面 RandomAccess 標記介面用於List實現,以表明它們支援快速(通常是恆定時間)的隨機訪問。該介面的主要目的是允許通用演算法改變其行為,以便在應用於隨機或順序訪問列表時提供良好的效能。

我們可以意識到,隨機訪問和順序訪問之間的區別往往是模糊的。例如,如果列表很大時,某些 List 實現提供漸進的訪問時間,但實際上是固定的訪問時間,這樣的 List 實現通常應該實現這個介面。作為一個經驗法則, 如果對於典型的類例項,List實現應該實現這個介面:

 for(int i = 0,n = list.size(); i <n; i ++)
         list.get(ⅰ);
複製程式碼

比這個迴圈執行得更快:

for(Iterator i = list.iterator(); i.hasNext();)
         i.next();
複製程式碼

上述 API 說有一個經驗法則,如果 for 遍歷某個 List 實現類的時候要比迭代器遍歷執行的快,就需要實現 RandomAccess 隨機快速訪問介面,標識這個容器支援隨機快速訪問。通過這個理論我們可以猜測,LinkedList 不具有隨機快速訪問的特性,換句話說LinkedList 的 for 迴圈遍歷要比 迭代器遍歷慢。下面我們來測試一下:

 private static void loopList(List<Integer> list) {
   long startTime = System.currentTimeMillis();

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

   System.out.println(list.getClass().getSimpleName() + "使用普通for迴圈遍歷時間為" +
           (System.currentTimeMillis() - startTime) + "ms");

   startTime = System.currentTimeMillis();

   Iterator<Integer> iterator = list.iterator();

   while (iterator.hasNext()) {
       iterator.next();
   }

   System.out.println(list.getClass().getSimpleName() + "使用iterator 迴圈遍歷時間為" +
           (System.currentTimeMillis() - startTime) + "ms");
}

 public static void main(String[] args){
    //測試 10000個整數的訪問速度
   List<Integer> arrayList = new ArrayList<Integer>(10000);
   List<Integer> linkedList = new LinkedList<Integer>();

   for (int i = 0; i < 10000; i++){
       arrayList.add(i);
       linkedList.add(i);
   }

   loopList(arrayList);
   loopList(linkedList);
   System.out.println();
}
複製程式碼

我們來看下輸出結果:

ArrayList使用普通for迴圈遍歷時間為6ms
ArrayList使用iterator 迴圈遍歷時間為4ms
LinkedList使用普通for迴圈遍歷時間為133ms
LinkedList使用iterator 迴圈遍歷時間為2ms
複製程式碼

可以看出 LinkedList 的 for迴圈的確耗費時間很長,其實這並不難理解,結合上一篇我們分析 LinkedList 的原始碼的時候,看到的 get(int index)方法 :

public E get(int index) {
   checkElementIndex(index);
   return node(index).item;
}
複製程式碼

node 方法內部根據 index 和 size/2 的大小作比較,來區分是從雙連結串列的頭節點開始尋找 index 位置的節點還是從尾部開始尋找,內部仍是 for 迴圈,而基於陣列資料結構的 ArrayList 則不同了,在陣列建立的時候,就可以很方便的通過索引去獲取指定位置的元素了。所以 ArrayList 具有隨機快速訪問能力,而LinkedList沒有。所以我們在使用 LinkedList 應儘量避免使用 for 迴圈去遍歷。

至此我們可以對 LinkedListArrayList 的區別做出總結:

  1. ArrayList 是底層採用陣列結構,儲存空間是連續的。查詢快,增刪需要進行陣列元素拷貝過程,當刪除元素位置比較靠前的時候效能較低。

  2. LinkedList 底層是採用雙向連結串列資料結構,每個節點都包含自己的前一個節點和後一個節點的資訊,儲存空間可以不是連續的。增刪塊,查詢慢。

  3. ArrayListLinkedList 都是執行緒不安全的。而 Vector 是執行緒安全的

  4. 儘量不要使用 for 迴圈去遍歷一個LinkedList集合,而是用迭代器或者高階 for

Stack 介紹

由開始的繼承體系可以知道 Stack 繼承自 Vector,也就是 Stack 擁有 Vector 所有的增刪改查方法。但是我們一說 Stack 肯定就是指棧這中資料介面。

我們先來看下棧的定義:

棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。

簡單來說,棧這種資料結構有一個約定,就是向棧中新增元素和從棧中取出元素只允許在棧頂進行,而且先入棧的元素總是後取出。 我們可以用陣列和連結串列來實現棧的這種資料結構的操作。

一般來說對於棧有一下幾種操作:

  1. push 入棧
  2. pop 出棧
  3. peek 查詢棧頂
  4. empty 棧是否為空

Java 中的 Stack 容器是以陣列為底層結構來實現棧的操作的,通過呼叫 Vector 對應的新增刪除方法來實現入棧出站操作。

// 入棧
public E push(E item) {
   addElement(item);//呼叫 Vector 定義的 addElement 方法

   return item;
}
// 出棧
public synchronized E pop() {
   E       obj;
   int     len = size();

   obj = peek();
   removeElementAt(len - 1);//呼叫 Vector 定義的 removeElementAt 陣列末尾的元素的方法 

   return obj;
}
// 查詢棧頂元素
public synchronized E peek() {
   int     len = size();

   if (len == 0)
       throw new EmptyStackException();
   return elementAt(len - 1);//查詢陣列最後一個元素。
}
複製程式碼

上邊簡單介紹了 Java 容器中的 Stack 實現,但是事實上官方並不推薦在使用這些陳舊的集合容器類。對於棧從資料結構上而言,相對於線性表,其實現也存在,順序儲存(陣列),非連續儲存(連結串列)的實現方法。而我們上一篇文章最後看到的 LinkedList 是可以取代 Stack來進行棧操作的。

最近在一個技術群裡,有一位美團大佬說他面試了一個位 Android 開發者,考察了一下這個 Android 開發者對於棧的理解,考察的題目是自己實現一個簡單棧,這個棧包含基本的peek ,push,pop 操作,結果不知道為何那個面試的人沒有寫出來,最終被 pass 掉了。所以在分析完 Stack 後,我決定自己手動嘗試寫一下這個面試題。我覺得我是面試官,如果回答者只寫出了出棧入棧的操作方法應該算是不及格的,面試官關注的應該是在寫 push 操作的時候有沒有考慮過 StackOverFlow 也就是棧滿的情況。

public class SimpleStack<E> {
    //預設容量
    private static final int DEFAULT_CAPACITY = 10;
    //棧中存放元素的陣列
    private Object[] elements;
    //棧中元素的個數
    private int size = 0;
    //棧頂指標
    private int top;


    public SimpleStack() {
        this(DEFAULT_CAPACITY);
    }

    public SimpleStack(int initialCapacity) {
        elements = new Object[initialCapacity];
        top = -1;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    @SuppressWarnings("unchecked")
    public E pop() throws Exception {
        if (isEmpty()) {
            throw new EmptyStackException();
        }

        E element = (E) elements[top];
        elements[top--] = null;
        size--;
        return element;
    }

    @SuppressWarnings("unchecked")
    public E peek() throws Exception {
        if (isEmpty()) {
            throw new Exception("當前棧為空");
        }
        return (E) elements[top];
    }

    public void push(E element) throws Exception {
        //新增之前確保容量是否滿足條件
        ensureCapacity(size + 1);
        elements[size++] = element;
        top++;
    }

    private void ensureCapacity(int minSize) {
        if (minSize - elements.length > 0) {
            grow();
        }
    }

    private void grow() {
        int oldLength = elements.length;
        // 更新容量操作 擴充為原來的1.5倍 這裡也可以選擇其他方案
        int newLength = oldLength + (oldLength >> 1);
        elements = Arrays.copyOf(elements, newLength);
    }
}

複製程式碼

同步 vs 非同步

對於 VectorStack 從原始碼上他們在對應的增刪改查方法上都使用 synchronized關鍵字修飾了方法,這也就代表這個方法是同步方法,執行緒安全的。而 ArrayListLinkedList 並不是執行緒安全的。不過我們在介紹 ArrayListLinkedList 的時候提及到了我們可以使用Collections 的靜態方法,將一個 List 轉化為執行緒同步的 List

List<Integer> synchronizedArrayList = Collections.synchronizedList(arrayList);
List<Integer> synchronizedLinkedList = Collections.synchronizedList(linkedList);
複製程式碼

那麼這裡又有一道面試題是這樣問的:

請簡述一下 VectorSynchronizedList 區別,

SynchronizedListCollections.synchronizedList(arrayList); 後生成的List 型別,它本身是 Collections 一個內部類。

我們來看下他的原始碼:

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
   private static final long serialVersionUID = -7754090372962971524L;

   final List<E> list;

   SynchronizedList(List<E> list) {
       super(list);
       this.list = list;
   }
   SynchronizedList(List<E> list, Object mutex) {
       super(list, mutex);
       this.list = list;
   }
   
   .....
}
複製程式碼

對於 SynchronizedList 構造可以看到有一個 Object 的引數,但是看到 mutex 這個單詞應該就明白了這個引數的含義了,就是同步鎖,其實我們點選 super 方法可以看到,單個引數的建構函式鎖就是其物件自身。

SynchronizedCollection(Collection<E> c) {
       this.c = Objects.requireNonNull(c);
       mutex = this;
   }

SynchronizedCollection(Collection<E> c, Object mutex) {
  this.c = Objects.requireNonNull(c);
  this.mutex = Objects.requireNonNull(mutex);
}
複製程式碼

接下來我們看看增刪改查方法吧:

   public E get(int index) {
       synchronized (mutex) {return list.get(index);}
   }
   public E set(int index, E element) {
       synchronized (mutex) {return list.set(index, element);}
   }
   public void add(int index, E element) {
       synchronized (mutex) {list.add(index, element);}
   }
   public E remove(int index) {
       synchronized (mutex) {return list.remove(index);}
   }

   public int indexOf(Object o) {
       synchronized (mutex) {return list.indexOf(o);}
   }
   public int lastIndexOf(Object o) {
       synchronized (mutex) {return list.lastIndexOf(o);}
   }

   public boolean addAll(int index, Collection<? extends E> c) {
       synchronized (mutex) {return list.addAll(index, c);}
   }
   //注意這裡沒加 synchronized(mutex)
   public ListIterator<E> listIterator() {
       return list.listIterator(); // Must be manually synched by user
   }

   public ListIterator<E> listIterator(int index) {
       return list.listIterator(index); // Must be manually synched by user
   }
複製程式碼

可以很清楚的看到,讓一個集合變成執行緒安全的,Collocations 只是包裝了引數集合的增刪改查方法,加了同步的限制。與 Vector 相比可以看出來,兩者第一個區別在於是同步方法還是同步程式碼塊,對於這兩個區別如下:

  1. 同步程式碼塊在鎖定的範圍上可能比同步方法要小,一般來說鎖的範圍大小和效能是成反比的。
  2. 同步塊可以更加精確的控制鎖的作用域(鎖的作用域就是從鎖被獲取到其被釋放的時間),同步方法的鎖的作用域就是整個方法。

由上述兩個方法看出來,``Collections.synchronizedList(arrayList);生成的同步集合看起來更高效一些,其實這種差異在 Vector 和 ArrayList上體現的很不明顯,因為其 add 方法內部實現大致相同。而從構造引數上來看Vector不能像SynchronizedList` 一樣指定加鎖物件。

而我們也看到了 SynchronizedList 並沒有給迭代器進行加鎖,但是翻看 Vector 的迭代器方法確實枷鎖的,所以我們在使用SynchronizedList的的迭代器的時候需要手動做同步處理:

  synchronized (list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext())
        foo(i.next());
 }
複製程式碼

至此我們可以總結出 SynchronizedListVector的三點差異:

  1. SynchronizedList 作為一個包裝類,有很好的擴充套件和相容功能。可以將所有的 List 的子類轉成執行緒安全的類。
  2. 使用 SynchronizedList 的獲取迭代器,進行遍歷時要手動進行同步處理,而 Vector 不需要。
  3. SynchronizedList 可以通過引數指定鎖定的物件,而 Vector 只能是物件本身。

總結

本文是繼 ArrayListLinkedList 原始碼分析完成後,針對List 這個家族進行的補充。我們分析了

  1. VectorArrayList 的區別。
  2. ArrayListLinkedList 的區別,引出了 RandomAccess 這個介面的定義,論證了 LinkedList 使用 for 迴圈遍歷是低效的。
  3. Stack 繼承自 Vector,操作也是執行緒安全的,但是同樣比較老舊。而後分析了實現一個簡單的 Stack 類的面試題。
  4. 最後我們從執行緒安全方面總結了 SynchronizedListVector的三點差異。

這些知識貌似都是面試官愛問的問題,也是平時工作中容易忽略的問題。通過這篇文章做出相應總結,以備不時之需。

相關文章