Java容器篇小結之List自問自答

I. List篇

0. 什麼是List

看到這個有點懵逼,一時還真不知道怎麼解釋,能讓徹底沒有接觸過的人都能聽懂java

列表,什麼是列表呢?數組

比如你到了一個村裏,看到每一個房子上的門牌號,如這是張村1號,下一個是張村2號,接着是3號、4號...安全

如今你要去張村10號,那麼得,順着這個門牌一直往下走,就能找到了數據結構

這個村落裏的門牌號的形式,就特別像JDK中的 LinkedList了,找張村10號的過程基本就是 LinkedList#indexOf()根據索引獲取對應值的過程了併發

從這個case裏面,小結一下List的特色性能

  • 順序性,有序(一個接一個,像冰糖葫蘆似的串在一塊兒)
  • 惟一性(一個蘿蔔一個坑,不容許一個索引撈出多個內容出來)

這個順序性好說,這個惟一性是我一家之言,多半描述得不太穩當,出發是爲了與Map進行對比 ,下面重點說明一下線程

以前的博文Java Map常見問題彙總小結 把Map比做了詞典,換在這裏比做村牌號,能夠這麼玩,將戶主名和門牌號一一對應,你報一個戶主名,我就告訴你他家門牌號,而後就有這麼個問題了設計

  • 張三家的門牌是張村五號
  • 後面張五家生了個小孩,也取名叫張三,而後張三自立門戶了,門牌分的是張村105號

如今我問張三家門牌是多少?是五號呢仍是105呢?這個就不惟一了code

換成鏈表的方式,你報一個門牌號,要麼這門牌號無效,要麼就只有一家在哪兒等着你呢,這就是我所說的惟一性對象

(廢話比較多,惋惜沒有稿費)


1. 常見的List有那些

JDK中實現了一些可以覆蓋絕大部分場景的List容器,羅列一些常見的以下

  • ArrayList
  • LinkedList
  • Vector
  • CopyOnWriteArrayList

2. 根據不一樣的分類方式,對上面的List進行劃分

根據是否線程安全分類

線程安全 非線程安全
Vector, CopyOnWriteArrayList ArrayList, LinkedList

根據獲取數據的時間複雜度

O(1) O(n)
ArrayList, Vector, CopyOnWriteArrayList LinkedList

這個分類有點意思了,前面三個,給了索引,立馬就把數據給你;這後面的一個,得遍歷一把找到對應的位置再返回數據,詳情建第四條


底層數據結構

數組 鏈表
ArrayList, Vector, CopyOnWriteArrayList LinkedList

擴容原理分類

動態擴容 無擴容一說
ArrayList, Vector, CopyOnWriteArrayList LinkedList

3. ArrayList怎麼用,如何實現的

基於數組的鏈表 ArrayList, 經常使用作有序數據存儲的容器,通常使用的三把斧

// 1. 建立數組
List<String> list = new ArrayList<>();

// 2. 添加數據
list.add("word");
list.add(0, "hello");

// 3. 獲取數組
list.get(0);

實現原理簡要說明

  • ArrayList底層數據結構爲數組
  • add(obj)會將數據直接扔進數組中,索引爲當前列表的長度size,而後size+1
  • 若數組已經滿了,塞不下新的數據了,此時須要將數組擴容,優先擴容原來容量的1.5倍(若依舊不夠,則擴容到剛好能容納全部元素)
  • add(index, obj), 在索引處添加數據,會致使原數組中,索引以後的數據後移(即會出現數組拷貝)
  • 刪除末尾數據,直接將其置爲null
  • 刪除數組內部的數據,會出現數組拷貝
  • 刪除元素,不會致使數組擴容(縮容)
  • 查詢索引位置內容,其實是直接利用數組的獲取方式

4. ArrayList, LinkedList區別與適用場景

二者的底層存儲結構不一樣,直接致使最終的使用場景的區別,下面以列表方式給出對比

說明 ArrayList LinkedList
存儲結構
隨機訪問 根據數組索引定位,耗時 O(1) 從鏈表頭or鏈表尾遍歷,耗時O(n)
新增數據 列表個數超過數組容量,則數組擴容,出現數組拷貝 定位到插入位置,新增一個節點插入鏈表中
是否擴容邏輯 插入超過上限擴容 <br/> 1. 增長原來空間大小的一半 <br/> 2. 若仍塞不下,則擴充到填滿 無擴容邏輯
應用場景 1. 適合隨機訪問 <br/> 2. 隨機插入or刪除致使數組拷貝 <br/> 3. 末尾插入,較友好 <br/> 1. 隨機訪問性能差,適合遍歷場景<br/> 2. 隨機插入or刪除,先遍歷獲取位置,而後插入or刪除<br/> 3. 鏈表頭尾插入友好 <br/>

5. ArrayList是否線程安全,如何保證線程安全

ArrayList 非線程安全,即在遍歷一個ArrayList對象時,若出現修改,則會拋一個併發修改異常,一般爲了保障線程安全,請使用 CopyOnWriteArrayList代替,至於Vector,已經退出歷史舞臺了

Vector

線程安全的列表,其實現是在全部的方法上加了同步鎖,確保同一時刻,只有一個線程在訪問列表,是一種僞併發的使用方式

CopyOnWriteArrayList

寫副本,替換原鏈表的方式實現線程安全,基本原理以下

  • 讀方法不加鎖;修改方法加鎖,一次只能一個寫線程訪問
  • 修改時,會拷貝一分內容出來,對拷貝的結果進行操做,最後覆蓋以前的內容
  • 遍歷和讀取都是基於訪問時刻列表中的數組進行的;在執行過程當中,鏈表發生修改不會影響遍歷和讀取的結果(即此時訪問的依然是原數組內容)

6. 幾種遍歷方式

把這個單獨拎出來有點混字數的感受(多謝也沒有啥收益,純屬手欠...)

通常的遍歷方式是採用foreach語法

List<String> strList = new ArrayList<>();
// ....
for(String str: strList) {
  System.out.println(str);
}

還有一種用得較少,通常是在要遍歷的過程當中,修改列表的值時使用

List<String> strList = new ArrayList<>();
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
    if(xxx) {
        iterator.remove();
    }
}

那麼問題來了,在第一種方式中,如果修改了會怎麼樣

@Test
public void testRemove() {
    List<String> strList = new ArrayList<String>();
    strList.add("hello");
    strList.add("world");
    strList.add("test1");
    strList.add("test2");
    strList.remove(0);
    int i = 0;
    for(String str: strList) {
        System.out.println(str);
        if(++i == 1) {
            strList.remove(i);
        }
    }
}

拋了併發修改異常

併發修改寫異常

7. 如何設計一個線程安全的ArrayList

徹底照着CopyOnWriteArrayList抄的話就沒意思了,然而讓本身去想一個方案,能夠怎麼搞?實現省略,暫時沒想法。。。

相關博文

II. Java分享,關注小灰灰Blog

相關文章
相關標籤/搜索