Java List 容器源碼分析的補充

Java List 容器源碼分析的補充

以前咱們經過分析源碼的方式學習了 ArrayList 以及 LinkedList 的使用方法。可是在分析源碼之餘,總免不了去網上查找一些相關資料,站在前人的肩膀上,發現前兩篇文章多多少少有些遺漏的地方,好比跟 ArrayList 很類似的 Vector 尚未說起過,因此本文想從面試中對於 List 相關問題出發,來填一填以前的坑,並對 List 家族中的實現類成員的異同點試着作出總結。java

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

Vector 介紹

Vector 是一個至關古老的 Java 容器類,始於 JDK 1.0,並在 JDK 1.2 時代對其進行修改,使其實現了 ListCollection 。從做用上來看,VectorArrayList 很類似,都是內部維護了一個能夠動態變換長度的數組。可是他們的擴容機制卻不相同。對於 Vector 的源碼大部分都和 ArrayList 差很少,這裏簡單看下 Vector 的構造函數,以及 Vector 的擴容機制。node

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() 返回一個 Enumerationbash

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 。其原理就是調用對應的迭代器的方法。dom

// 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的三點差別。

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

相關文章
相關標籤/搜索