Java容器(二)——「ArrayList、LinkedList性能測試與分析」

測試目標

集合中最常用的就是List,用於存儲可重複的數據集。
爲了瞭解List的幾個實現類的性能區別以及使用場景,進行簡單的性能測試對比。
我們常規對於ArrayList、LinkedList的認知是:
1. 數據查詢的效率(Get的效率)
ArrayList:使用數組,獲取數據通過數組下標進行快速定位,性能爲O(1),數據順序、隨機獲取的速度快。
LinkedList:使用鏈表進行數據存儲,查詢數據需要從頭或者從尾部遍歷鏈表,性能O(n),因此數組數據越多、查詢位置越靠中間性能越差。
2. 數據添加的效率(Add的效率)
ArrayList:數組的數據添加,如果在頭部或者中間性能最差,因爲需要更改後面所有數據的位置和索引。如果在尾部性能則影響不大。
LinkedList:插入的效率比ArrayList和Vector都高,因爲只需要更改相應位置前後節點的指向就行。
3. 數據移除的效率(Remove的效率)
數據移除同添加
4. 數據替換的效率(Set的效率)
ArrayList:數據替換由於不需要更改後續數組的位置和索引,因此效率比插入高。同時LinkedList需要定位位置,因此ArrayList也會比LinkedList快。
LinkedList:需要定位替換的位置,效率較低。

根據這些認知,我們可以得到假設:
1. ArrayList
不適宜應用於經常需要在集合中間或者首位進行插入或者刪除操作的場景,適宜應用於讀取操作多的場景。
2. LinkedList
不適宜應用於隨機位置進行讀取的場景,適宜於需要在集合中進行插入或者刪除的場景。

測試目標:
1. 測試在頭、尾進行插入的效率
2. 測試在集合中間隨機位置進行隨機插入的效率
3. 測試在從集合隨機位置獲取數據的效率
4. 測試從隨機位置替換數據的效率
5. 測試隨機位置的數據移除操作

測試代碼

ListNature.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * @author lzy
 * @date 2018/1/17
 */
public class ListNature extends BaseNature{

    private static Logger logger = LoggerFactory.getLogger(ListNature.class);

    /**
     *
     * @param listSize
     * @param removeCount
     */
    public void testRemoveRandom(int listSize, int removeCount) {
        if (listSize < removeCount) {
            throw new IllegalArgumentException("removeCount需要小於listSize");
        }
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        testAddLast(arrayList, listSize);
        System.gc();
        long arrayRemove = testRemoveRandom(arrayList, removeCount);
        arrayList.clear();
        System.gc();
        testAddLast(linkedList, listSize);
        System.gc();
        long linkedRemove = testRemoveRandom(linkedList, removeCount);
        linkedList.clear();
        System.gc();

        logger.debug("於{}集合數據量下,{}次測試Array與LinkedList測試隨機刪除,結果如下:===",listSize,removeCount);
        logger.debug("ArrayList:耗時{}",arrayRemove);
        logger.debug("LinkedList:耗時{}",linkedRemove);

    }

    /**
     *
     * @param listSize
     * @param replaceCount
     */
    public void testReplaceRandom(int listSize, int replaceCount) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        testAddLast(arrayList, listSize);
        long arrayReplace = testReplaceRandom(arrayList, replaceCount);
        arrayList.clear();
        System.gc();
        testAddLast(linkedList, listSize);
        long linkedReplace = testReplaceRandom(linkedList, replaceCount);
        linkedList.clear();
        System.gc();

        logger.debug("於{}集合數據量下,{}次測試Array與LinkedList測試隨機替換,結果如下:===",listSize,replaceCount);
        logger.debug("ArrayList:耗時{}",arrayReplace);
        logger.debug("LinkedList:耗時{}",linkedReplace);
    }

    /**
     * 測試ArrayList和LinkedList集合隨機獲取數據的效率
     *
     * @param listSize 集合原始容量
     * @param getCount 獲取次數
     */
    public void testGetRandom(int listSize, int getCount) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        testAddLast(arrayList, listSize);
        long arrayGet= testGetRandom(arrayList, getCount);
        arrayList.clear();
        System.gc();
        testAddLast(linkedList, listSize);
        long linkedGet = testGetRandom(linkedList, getCount);
        linkedList.clear();
        System.gc();

        logger.debug("於{}集合數據量下,{}次測試Array與LinkedList測試隨機獲取,結果如下:===",listSize,getCount);
        logger.debug("ArrayList:耗時{}",arrayGet);
        logger.debug("LinkedList:耗時{}",linkedGet);
    }

    /**
     * 測試ArrayList和LinkedList集合中隨機插入數據的效率
     *
     * @param listSize    數組原始容量
     * @param insertCount 插入次數
     */
    public void testInsertRandom(int listSize, int insertCount) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        testAddLast(arrayList, listSize);
        long arrayInsert = testInsertRandom(arrayList, insertCount);
        arrayList.clear();
        System.gc();
        testAddLast(linkedList, listSize);
        long linkedInsert = testInsertRandom(linkedList, insertCount);
        linkedList.clear();
        System.gc();

        logger.debug("於{}集合數據量下,{}次測試Array與LinkedList測試隨機添加,結果如下:===",listSize,insertCount);
        logger.debug("ArrayList:耗時{}",arrayInsert);
        logger.debug("LinkedList:耗時{}",linkedInsert);
    }

    /**
     * 測試ArrayList和LinkedList集合中首尾插入數據的效率
     *
     * @param addCount 插入次數
     */
    public void testAddFirstAndLast(int addCount) {
        List<String> arrayList = new ArrayList<>();
        List<String> linkedList = new LinkedList<>();
        long arrayListAddFirst =  testAddFirst(arrayList, addCount);
        System.gc();
        long arrayListAddLast = testAddLast(arrayList, addCount);
        arrayList.clear();
        System.gc();
        long linkedListAddFirst = testAddFirst(linkedList, addCount);
        System.gc();
        long linkedListAddLast = testAddLast(linkedList, addCount);
        linkedList.clear();
        System.gc();

        logger.debug("{}次測試Array與LinkedList測試頭尾添加,結果如下:===",addCount);
        logger.debug("ArrayList:頭部添加耗時{},尾部添加耗時{}",arrayListAddFirst,arrayListAddLast);
        logger.debug("LinkedList:頭部添加耗時{},尾部添加耗時{}",linkedListAddFirst,linkedListAddLast);
    }

    private long testAddFirst(List list, int addCount) {
        int[] array = getRandomArray(addCount);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < addCount; i++) {
            list.add(0, array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }

    private long testAddLast(List list, int addCount) {
        int[] array = getRandomArray(addCount);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < addCount; i++) {
            list.add(array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }

    private long testInsertRandom(List list, int insertCount) {
        int[] array = getRandomArray(insertCount, list.size());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < insertCount; i++) {
            list.add(array[i], array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }

    private long testReplaceRandom(List list, int replaceCount) {
        int[] array = getRandomArray(replaceCount, list.size());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < replaceCount; i++) {
            list.set(array[i], array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }

    private long testGetRandom(List list, int getCount) {
        int[] array = getRandomArray(getCount, list.size());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < getCount; i++) {
            list.get(array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }

    private long testRemoveRandom(List list, int removeCount) {
        int[] array = getRandomArray(removeCount, list.size() / 100);
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < removeCount; i++) {
            list.remove(array[i]);
        }
        long time2 = System.currentTimeMillis();
        long interval = time2 - time1;
        return interval;
    }
}

ListNatureTest.java

import org.junit.Test;

/**
 * @author lzy
 * @date 2018/1/18
 */
public class ListNatureTest {

    @Test
    public void testAdd() {
        int[] testCount = {10000, 50000,100000};
        ListNature natureTest = new ListNature();
        for (int i : testCount) {
            natureTest.testAddFirstAndLast(i);
        }
    }

    @Test
    public void testInsert() {
        int[] testCount = {10000, 50000,100000};
        int[] listSize = {10000, 50000,100000};
        ListNature natureTest = new ListNature();
        for (int i : testCount) {
            for (int j : listSize) {
                natureTest.testInsertRandom(j, i);
            }
        }
    }

    @Test
    public void testGet() {
        int[] testCount = {10000, 50000,100000};
        int[] listSize = {10000, 50000,100000};
        ListNature natureTest = new ListNature();
        for (int i : testCount) {
            for (int j : listSize) {
                natureTest.testGetRandom(j, i);
            }
        }
    }

    @Test
    public void testReplace() {
        int[] testCount = {10000, 50000,100000};
        int[] listSize = {10000, 50000,100000};
        ListNature natureTest = new ListNature();
        for (int i : testCount) {
            for (int j : listSize) {
                natureTest.testReplaceRandom(j, i);
            }
        }
    }

    @Test
    public void testRemove() {
        int[] testCount = {10000, 50000};
        int[] listSize = {100000,200000};
        ListNature natureTest = new ListNature();
        for (int i : testCount) {
            for (int j : listSize) {
                natureTest.testRemoveRandom(j, i);
            }
        }
    }

}

測試結果

測試的結果影響因素很多,但是我們主要是定性測試,因此不要糾結於具體的時間毫秒數,而是要了解每個組件的性能趨勢。

1.查詢測試

數據的查詢考慮的是查詢次數以及數組長度兩個維度,因此各自使用1w、5w、10w三種測試場景,單位毫秒(ms)。

A:ArrayList
L:LinkedList

測試次數 A(1w) L(1w) A(5w) L(5w) A (10w) L(10w)
1w 1 99 1 552 1 1156
5w 1 285 1 1649 1 6133
10w 1 1079 1 4773 1 11421

2.添加測試

A首:ArrayList首位添加
L首:LinkedList首位添加
A尾:ArrayList的尾部添加
L尾:LinkedList的尾部添加
結果1:

測試次數 A首(ms) L首(ms) A尾(ms) L尾(ms)
1w次 15 4 1 2
5w次 210 6 4 2
10w次 899 2 8 1

測試在數組中隨機插入,主要需要考慮數組的原本的長度,因此取三個較大的數組長度的放大兩者之間的差距1w、10w、100w
A中:在N條數據內的隨機插入,ArrayList的中間隨機插入
L中:LinkedList的中間隨機插入
結果2:

測試次數 A中(1w) L中(1w) A中(5w) L中(5w) A中 (10w) L中(10w)
1w次 19 480 51 1734 92 1685
5w次 222 4523 307 13740 687 21928
10w次 935 9287 1676 49533 2648 82517

3.刪除測試
原計劃是分隊列頭部、尾部、隨機位置三個場景測試,考慮到頭部刪除、尾部刪除於頭尾插入類似,因此只需要測試隨機位置刪除即可。測試需要考慮數組的原本的長度以及刪除的數據數量,即刪除數量需要大於數組原本長度。因此測試數據量爲10w、100w

測試次數 A(10w) L(10w) A (20w) L(20w)
1w次 105 17 216 34
5w次 349 79 949 211

4.替換測試
直接上替換測試結果:

測試次數 A(1w) L(1w) A(5w) L(5w) A (10w) L(10w)
1w次 3 81 1 308 1 742
5w次 2 337 1 2160 1 4792
10w次 1 831 4 4818 1 10023

測試結果分析

從測試結果反應的時間來看,可以得到以下幾點結論:

  1. ArrayList無論在多大的數據量的情況下,獲取元素的效率都相差不大。隨機獲取的效率總體上比LinkedList高
  2. LinkedList和ArrayList在尾部添加數據的效率相差不大,頭部添加則LinkedList明顯優於ArrayList
  3. ArrayList元素隨機替換效率和查詢效率相同,都比較高。LinkedList效率較低。
  4. 隨機插入的效率ArrayList反而比LinkedList高

源碼分析

1. Get對比
ArrayList的get非常簡單,伴隨着一個數組範圍檢查後直接是範圍相應位置的元素,非常的簡單高效。



public E get(int index) {
    //判斷 index<size,否則拋出IndexOutOfBoundsException
    rangeCheck(index);
    return elementData(index);
}

LinkedList的get通過鏈表的頭或者尾進行遍歷,直到找到index位置的對象。

public E get(int index) {
    //判斷index>=0&&index<size,否則拋出IndexOutOfBoundsException
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

從兩者的Get實現角度看,集合中元素的數量對於ArrayList的獲取性能影響不大,性能比較穩定。而LinkedList的效率則關乎獲取元素的位置,離集合的頭或尾越近,性能越好,反之則越差。

2.Add對比
ArrayList 在順序插入時,如果數據容量不夠,會經常擴容,其中擴容代碼Arrays.copyOf(elementData, newCapacity); 會消耗大量時間,但如果數據量較大,此時擴容次數明顯下降(擴容總是會在當前容量的1.5倍),因此擴容消耗的時間平均下來明顯降低。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//擴容判斷
private void ensureExplicitCapacity(int minCapacity) {
    //用於Fail-Fast判斷
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 新的數組長度爲原數組的長度*1.5,若是小於最小長度則賦值爲最小長度。若大於最大長度(Integer.MAX_VALUE-8),則賦值爲Integer.MAX_VALUE
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);
}

LinkedList 順序插入,先新增一個前節點,綁定前一節點爲last,最後把新增節點置爲last。

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

3. add(int index, E element)中間插入對比
ArrayList需要進行數組拷貝,把index位置之後的元素整體後移一位,如果數組較長且index位置靠前(即index之後元素較多),會浪費大量時間,因此插入時間會呈指數級增長。

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

LinkedList的數據插入,主要消耗在index位置的鏈表定位上,index位置的查找速度越快,則插入速度越快。

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

remove也是同理,ArrayList主要是拷貝,LinkedList主要是定位。
set對於ArrayList同get,而LinkedList主要是定位。

綜合以上各項數據,對於兩個集合使用有以下建議:
1、大部分的情況下使用ArrayList即可
2、在數組長度能夠確定的情況下,使用ArrayList更優,避免了數組擴容
3、頻繁添加刪除(在 list 開始部分),但不需要頻繁訪問,可以考慮使用LinkedList
4、分不清場景下需要使用哪一種的,建議結合着業務做一下測試。

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