深入理解 ArrayList、LinkedList、HashSet 等 Java 容器

轉載請註明原創出處,謝謝!

HappyFeet的博客

Java 容器的繼承關係圖:

java容器的繼承關係圖

集合表示一組對象,稱爲其元素。有些集合允許重複元素,而另一些則不允許。有些是有序的,有些是無序的。 JDK 沒有提供這個接口的任何直接實現:它提供了更具體的子接口(如 Set、List 和 Queue)的實現。


一、List

有序的元素序列,可以通過索引(即下標)訪問元素。

1、ArrayList(基於索引的動態數組)

(1)實現了可變大小的數組,允許所有元素,包括 null 值,底層使用數組(array)保存所有元素,所以隨機訪問很快,可以直接通過元素的下標值獲取元素的值(size、isEmpty、get、set、iterator、listIterator 這些方法的時間複雜度均爲 O(1)),但插入和刪除較慢,因爲需要移動 array 裏的元素(即 add、remove 的時間複雜度爲 O(n)),未實現同步。

(2)每一個 ArrayList 實例都有一個容量(capacity),使用 Lists.newArrayList() 創建的是一個 capacity = 0 的 List,當在添加第一個元素的時候會擴展到默認的初始化容量(10),當對其添加的數據大於它的 capacity 就必須改變 ArrayList 的 capacity(一般是原來大小的 1.5 倍),而這種 resize 操作是有開銷的,所以如果事先知道數組的大小爲 actualSize,可以按照下面的方式初始化一個大小固定的 ArrayList,以減去 resize 的開銷:

int actualSize = 100;
List<Object> objectArrayList = Lists.newArrayListWithCapacity(actualSize);

(3)iterator() 和 listIterator(int) 返回的迭代器是快速失敗(fail-fast)的:

如果在迭代器創建之後,原始的 List 被修改了,迭代器會拋一個 ConcurrentModificationException,原因是 Iterator 裏的 expectedModCount 和 List 的 modCount 不一致。在迭代的時候如果需要修改 List,只能通過 Iterator 的 remove 方法修改。

(4)從 Array 創建 ArrayList 的坑:

Object[] array = new Object[10];
List<Object> arrayList1 = Lists.newArrayList(Arrays.asList(array));
List<Object> arrayList2 = Arrays.asList(array);
// 需要注意的是:Arrays.asList(array) 返回的是一個 fixed size array(上面的arrayList2),如果不用 Lists.newArrayList(Arrays.asList(array))(上面的arrayList2)包裝起來的話,對它進行 add 或 remove 操作就會報 java.lang.UnsupportedOperationException

2、LinkedList(雙鏈表數據結構)

(1)實現了 List 接口,允許 null 值,底層使用鏈表保存所有元素(除了要存數據外,還需存 next 和 pre 兩個指針,因此佔用的內存比 ArrayList 多),因此,向 LinkedList 裏面插入或移除元素時會特別快,但是對於隨機訪問方面相對較慢(需要遍歷鏈表,遍歷的時候會根據 index 選擇從前往後或從後往前遍歷,如果 index < (size >> 1) 則從前往後),無同步,想要實現同步可以這樣:

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

(2)LinkedList 還擁有了可以使其用作堆棧(stack),隊列(queue)或者雙向隊列(deque)的方法(擁有 pop、push,從 LinkedList 的首部或尾部添加或刪除元素等方法)。

3、Vector(實現了同步的 ArrayList)

和 ArrayList 幾乎一模一樣,除開以下兩點:

  • 實現了同步,較 ArrayList 有輕微的性能上的差距(一般不用它,而是使用 ArrayList,在外部實現同步);

  • 二者的 resize 的大小不一樣:ArrayList 是變爲原來的 1.5 倍,而 Vector 爲原來的 2 倍。

4、Stack(Java 的堆棧實現是糟糕的,它繼承了 Vector)

Stack 表示後進先出(LIFO)堆棧,繼承於 Vector ,新增了五個方法:

  • push(E item):將 item 壓入棧;

  • pop():remove 掉棧頂元素並返回 remove 掉的元素;

  • peek():返回棧頂(Vector 的最後一個元素)的第一個元素(無 remove 操作);

  • empty():判斷棧是否爲空;

  • search():返回查找到的離棧頂最近的元素的 position;

  • javadoc 中建議:Deque 接口和它的實現提供了一個更完整、更一致的 LIFO 棧操作集,應該優先使用這個類。例如:

Deque<Integer> stack = new ArrayDeque<Integer>();

5、ArrayList、LinkedList 和 Vector 總結

(1)當集合內的元素需要頻繁插入,刪除操作時應使用 LinkedList;當需要頻繁查詢時,使用 ArrayList(大部分情況是使用 ArrayList);

(2)ArrayList 和 LinkedList 都未實現同步,Vector 是在 ArrayList 的基礎上實現了同步,是線程安全的;

(3)相比而言,LinkedList 佔的內存要比 ArrayList 大(因爲它必須維護下一個和前一個節點的鏈接)。

二、Set

不包含重複元素的集合。

1、HashSet

  • 由 HashMap 支持實現,無序,未實現同步,允許 null 值;

  • add, remove, contains and size:這些方法的時間複雜度爲 O(1),迭代 HashSet 的時間與實際 HashSet 的 size 和內部支持實現的 HashMap 的 capacity 之和成線性關係。所以,如果對迭代性能敏感,就不要把 HashSet 的初始容量設置太高(或者負載因子太低)(實際上是減小 HashMap 的 capacity 值)。

2、LinkedHashSet

  • 由 LinkedHashMap 支持實現,有序(保證了元素的插入順序),未實現同步,允許 null 值

  • 與 HashSet 相比:需要多維護一個 linked list ,所以總體上性能上會比 HashMap 稍微慢一點,但有一點例外:迭代 LinkedHashSet 的開銷比迭代 HashSet 要小,原因是迭代 LinkedHashSet 只與 size 相關,而迭代 HashSet 還與 capacity 相關。

3、SortedSet

  • 元素有序(按照自然排序或 Comparator 排序)

  • 內部的元素都必須實現 Comparable 接口,保證集合內部任意兩個元素之間是可比較的

  • subSet(E fromElement, E toElement) :返回該集合部分元素([from,to))的視圖(view),操作該視圖會映射到原集合上,而且該視圖有限制:當添加一個此範圍([from,to))之外的值會拋 IllegalArgumentException。

  • headSet(E toElement):( < toElement) :返回一個和 subSet 一樣的視圖([lowestElement, toElement))

  • tailSet(E fromElement):( >= fromElement) :返回一個和 subSet 一樣的視圖([fromElement, highestElement])

4、TreeSet(SortedSet 的實現)

  • 基於 TreeMap 的 NavigableSet 實現;未實現同步。

  • add, remove and contains :時間複雜度爲 log(n)

三、Queue

爲在處理之前保存元素而設計的集合。

  • 提供了額外的插入(offer)、提取(poll)和檢查(peek)操作,當操作失敗時返回 null 或 false,而不是拋出異常。

  • 一般來說,隊列都是先進先出(FIFO)的方式,但也有例外:如優先級隊列(PriorityQueue)。

  • 隊列實現通常不允許插入空元素,因爲 null 被 poll 方法用作特殊返回值來指示隊列不包含任何元素。

1、PriorityQueue

基於優先級堆(實際上是最小堆)實現,用數組存儲堆;

  • 未實現同步(線程安全的優先級隊列:PriorityBlockingQueue),是無界隊列,但有容量(capacity),添加元素時,如果容量不足,會自動增長。

  • iterator() 方法不提供保證以特定的順序遍歷優先級隊列中的元素。

  • offer, poll, remove() and add 的時間複雜度爲 O(log(n)):往堆裏增刪元素

  • remove(Object)contains(Object) 爲線性時間:Java 文檔上是寫的這兩個方法的複雜度都是線性時間,即 O(n) ,因爲是用數組存儲堆的,所以 contains 就是遍歷數組查找元素,因此,contains(Object) 的複雜度是 O(n) 是沒有問題的,但是我覺得 remove(Object) 操作的複雜度是 O(n) + O(log(n)),即從數組裏面找到它 O(n),然後從堆裏面刪除它 O(log(n))。(還是我理解錯了,希望有大佬能夠指導一下?)

  • peek, element, and sizeO(1):因爲是取堆頂元素。

2、Deque

雙向隊列,支持兩端元素的插入與刪除。Deque 也可以用作 LIFO(後進先出)堆棧。這個接口應優先於傳統的 Stack 類使用。

不支持通過索引訪問元素。

雖然 Deque 實現不是嚴格要求禁止插入 null 值,強烈建議任何允許 null 元素的 Deque 實現的用戶不要利用插入空值的能力,因爲null被一些方法用作特殊返回值來指示該雙端隊列是空的。

3、ArrayDeque(Deque 的實現)

  • 未實現同步,不允許 null 值。

  • 當用作堆棧時,該類可能比 Stack 快,並且在用作隊列時比 LinkedList 快。

參考資料:

(1)Create ArrayList from array

(2)When to use LinkedList over ArrayList?

(3)java中的容器講解

(4)Java ArrayList resize costs

(5)collections in java

(6)Why should I use Deque over Stack?

(7)Java 文檔

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