深入淺出LinkedList、ArrayList

1. ArrayList底層實現

ArrayList底層是一個動態數組,即容量可變,ArrayList繼承了抽象List實現了諸如以下的接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

實現了RandomAccess可以使ArrayList實現快速隨機訪問(通過數組下表標),實現了Cloneable可以使ArrayList被克隆,Serializable可以使ArrayList實現序列化,其中最關鍵的部分:

transient Object[] elementData;

即ArrayList底層是一個transient 修飾的Object數組

問題1:爲什麼用transient 修飾?

transient:Java關鍵字,變量修飾符,如果用transient聲明一個實例變量,當對象存儲時,用transient關鍵字標記的成員變量不參與序列化過程

問題2:爲什麼用transient:關鍵字使其不支持序列化?

假如elementData的長度爲10,而其中只有5個元素,那麼在序列化的時候只需要存儲5個元素,而數組中後面5個元素是不需要存儲的**,即存在一定的空間浪費!**。於是將elementData定義爲transient,避免了Java自帶的序列化機制,但是其自定義了兩個方法,實現了自己可控制的序列化操作。

private void writeObject(java.io.ObjectOutputStream s)  
private void readObject(java.io.ObjectInputStream s)  

2. ArrayList構造函數

ArrayList默認的無參構造方法:

    public ArrayList() {
        //無參構造器默認構造一個空數組
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以在設置時指定初始值大小initialCapacity的值即

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //小於零則拋出錯誤
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

3. ArrayList重要方法

首先就是add添加的方法:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

這裏我們看到每一次添加的時候先去調用ensureCapacityInternal的方法,我們點進去看看:

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
         // 數據結構發生改變,和fail-fast機制有關(下面會講),在使用迭代器過程中,只能通過迭代器的方法(比如迭代器中add,remove等),修改List的數據結構,
    // 如果使用List的方法(比如List中的add,remove等),修改List的數據結構,會拋出ConcurrentModificationException
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

這裏ensureExplicitCapacity又調用了grow的方法,我們再次點進去:

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

即add方法中,ArrayList每次新增元素都會進行容量大小檢測判斷,若新增的後元素的個數會超過ArrayList的容量,就會調用ensureCapacity->進而調用grow的擴容方法進行擴容滿足新增元素的需求

爲什麼是擴容成1.5倍?

假設現在我有11個元素,擴容成15之後有四個是浪費掉的即

在這裏插入圖片描述

浪費了26%還多

如果是2.5倍呢?此時就會浪費更多:56%!

3.5、4.5就更多了,但是假設現在是1.1呢,我們知道擴容調用的是Arrays.copyOf(elementData, newCapacity)即複製的方法,每一次複製都要從頭到尾複製一遍,太多的複製次數影響性能,所以1.5倍是一個折中的辦法:即減少了空間的浪費也減少了性能的損耗!

4. 快速失敗

java.util下的包都是快速失敗的,何爲快速失敗?

即內部維護了一個modCount的值,上面也反覆提到了,每一次遍歷的時候都會講modCout的值與內存值進行比較,假設內存值!=modCount,即其他線程對當前的集合進行了修改,就直接拋出一個ConcurrentModificationException

ArrayList源碼中大量運用了checkForComodification方法

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

安全失敗

與快速失敗相對應的是安全失敗,採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷,這樣就不會直接扔出一個異常了。

1. LinkedList
2. ArrayList

那麼這兩者有什麼異同呢
首先是代碼測試:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import bean.Answer;
import bean.User;

/**
 * 測試類
 * 
 * @author hxz
 *
 */
public class MyTestUtil {
	
	public static void addTest(List<String> list) {
		System.out.println(list.getClass().getName() + "開始查詢");
		long start = System.currentTimeMillis();
		System.out.println("開始時間:" + start);
		list.add("需要查找的數據");
		list.add("eeee");
		list.add("aaee");
		list.add("abbb");
		for (int i = 0; i < 10000000; i++) {
			for (int j = 0; j < list.size(); j++) {
				list.get(j).contains("e");
				System.out.print("");
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("結束時間:" + end);
		System.out.println("總耗時:" + (end - start));
	}

	public static void main(String[] args) {
		List<String> a = new ArrayList<>();
		List<String> b = new LinkedList<>();
		addTest(a);
		addTest(b);
	}
}


在這裏插入圖片描述

???不是說ArrayList查詢快於LinkedList麼?

然後我們測試增加
在這裏插入圖片描述
???簡直顛覆我的認知!
難道我學的都是錯的嗎?
事情的真相只有一個!
首先我們需要了解
linkedLIst是雙向鏈表結構
在這裏插入圖片描述
元素之間的所有關係是通過引用關聯的,就好比最近特別火的從袖子裏撤出棒棒糖來的情景,想要撤出下一個就必須撤出上一個。它在查詢的時候,只能一個一個的遍歷查詢,所以他的查詢效率很低,如果我們想刪除一節怎麼辦呢?就相當於自行車的鏈子,有一節壞了,我們是不是直接把壞的那節仍掉,然後讓目標節的上一節指向目標節的下一節,但是
ArrayList是數組結構
在這裏插入圖片描述
就是有相同特性的一組數據的箱子,比如說我有一個能容下10個蘋果的箱子,我現在只放了5個蘋果,那麼放第6個是不是直接放進去就行了?呢我要放11個呢?這個箱子是不是放不下了?所以我是不是需要換個大點的箱子?這就是**數組的擴容!**同樣,我們一般放箱子裏面的東西是不是按照順序放的?假如說是按abcd的順序放的,我突然想添加一個e,這個e要放到c的後面,你是不是需要把d先拿出來,再把e放進去,再把d放進去?假如說c後面有10000個呢?你是不是要把這10000個都拿出來,把e放進去,再放這10000個?效率是不是很低了?所以,理論上它的增刪比較慢!但是前面也說了,我們箱子裏面放東西,都是按照順序放的,所以我知道其中一個"地址",是不是就知道所有元素的地址?所以它的查詢在理論上比較快!
注意:只是在在list容量較大情況下,ArrayList查詢數據要遠優於LinkedList

在這裏插入圖片描述
在這裏插入圖片描述
總結
ArrayList和LinkedList在性能上各有優缺點,都有各自所適用的地方,總的說來可以描述如下:
對ArrayList而言,主要是在內部數組中增加一項數據,指向所添加的元素,偶爾複雜度爲O(n)可能會導致對數組重新進行分配;而對LinkedList而言,這個開銷是統一的,複雜度爲O(1),分配一個內部對象。
在ArrayList的中間插入或刪除一個元素意味着這個列表中剩餘的元素都會被移動;而在LinkedList的中間插入或刪除一個元素的開銷是固定的。

  1. LinkedList不支持高效的隨機元素訪問。
  2. 只是在在list容量較大情況下,ArrayList查詢數據要遠優於LinkedList
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章