Java基礎(chǔ)——集合(1)ArrayList詳解 —— 悟道

創(chuàng)建ArrayList


創(chuàng)建 ArrayList 時窿给,一般是直接創(chuàng)建覆获,或者是直接將實現(xiàn)了 Collection 接口的集合類傳入進行初始化創(chuàng)建

// 1 直接new
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(Arrays.toString(list.toArray()));

// 2 創(chuàng)建時直接添加元素 實現(xiàn)了collection接口的類
ArrayList<String> list1 = new ArrayList<>(list);
System.out.println(Arrays.toString(list1.toArray()));
HashSet<String> hashSet = new HashSet<>();
hashSet.add("C");
ArrayList<String> list2 = new ArrayList<>(hashSet);
System.out.println(Arrays.toString(list2.toArray()));

添加元素 -> ArrayList 的擴容機制

首先看 ArrayList 的 add 方法,當添加元素時贸弥,首先是通過 ensureCapacityInternal 方法進行

增量判斷窟坐,判斷后進行賦值以及返回添加元素成功。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!這里是增量機制
    elementData[size++] = e;
    return true;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final int DEFAULT_CAPACITY = 10;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

而對于 ensureCapacityInternal 方法來說绵疲,首先通過 calculateCapacity 方法來計算容量哲鸳。

如果 elementData(存儲數(shù)據(jù)的數(shù)組) 為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 數(shù)組,

則說明 ArrayList 是無參剛創(chuàng)建的盔憨,所以使用默認的容量(10)與實際數(shù)量 minCapacity(size + 1)

中最大值徙菠。通過 ensureExplicitCapacity 方法驗證容量與 elementData 實際長度的大小情況,

通過 grow 方法進行擴容郁岩。同理 addAll 方法也是同樣的機制婿奔。只不過多了復制數(shù)組的一步。

成員變量 modCount 則是后面再說问慎,下面先說 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);
}

grow 方法增容詳解。首先根據(jù) ArrayList 中元素的數(shù)量 + (數(shù)量 >> 1)也就大約是容量的1.5倍如叼,

所以 newCapacity - minCapacity < 0 這個條件只有空數(shù)據(jù)第一次添加的時候能成立冰木,

讓 elementData 初始化為長度10的數(shù)組,之后都不成立笼恰。在上面的 ensureExplicitCapacity

方法中 minCapacity 是 size + 1踊沸,所以只有當 size == elementData.length 時,才會

調(diào)用 grow 方法進行擴容社证,將容量 增擴為 oldCapacity + (oldCapacity >> 1)逼龟,約是1.5倍。

刪除元素 -> ArrayList 刪除元素時所需考慮問題

刪除方法一般使用 remove 方法猴仑,其他較為特殊的方法暫不討論审轮。

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

通過 index 刪除時先進行下標范圍檢查肥哎,然后獲取到刪除元素,移動原數(shù)組疾渣。

為什么是 size - 1 - index篡诽,是因為 index 從0開始。numMoved 不判斷=0的情況

則是因為=0時榴捡,刪除元素是末尾元素杈女,直接置 null 就好。elementData[--size] = null

就是末尾元素置 null吊圾。其他情況則移動元素通過 object 刪除時則涉及到 fastRemove 方法达椰,

原理和刪除 index 的情況一致。這里判斷了 o == null是因為 o 為 null 時調(diào)用

equals 方法時報 NPE项乒,所以要判斷啰劲。而 equals 方法判斷相等時,為了實現(xiàn)現(xiàn)實中的

‘相等概念’需要重寫 equals 和 hashCode 方法檀何。否則 equals 只是判斷是否為同一對象蝇裤。

ArrayList<String> list = new ArrayList<>();
list.add("張三");
list.add("張三");
list.add("趙四");
list.add("王五");

for (int i = 0; i < list.size(); i++) {
    if ("張三".equals(list.get(i))) {
        list.remove(list.get(i));
        System.out.println("張三已被刪除");
    }
}

for (String str : list) {
    if ("張三".equals(str)) {
        list.remove(str);
        System.out.println("張三已被刪除1");
    }
}

/*
    張三已被刪除
    張三已被刪除1
    Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
    at java.util.ArrayList$Itr.next(ArrayList.java:861)
    at com.wuhaitao.base.collection.List.ArrayListInfo.main(ArrayListInfo.java:62)
*/

一般情況下,集合中刪除元素時频鉴,我們都是通過遍歷去刪除元素栓辜。所以這里存在一些問題。

由于有fail-fast 機制垛孔,這個機制意思是:一旦檢測到可能會發(fā)生錯誤藕甩,就立馬拋出異常,

程序?qū)⒉辉偻聢?zhí)行周荐。這個機制并不是集合中特有的狭莱,只是在集合中很容易實現(xiàn),在這里

介紹很方便概作。比如上面的例子贩毕,看著沒有任何問題,但是一旦運行起來就會報錯仆嗦,拋出了

ConcurrentModificationException 異常。并且還調(diào)用了911行的checkForComodification

方法先壕。明明我們只調(diào)用了 remove 方法瘩扼,remove 方法中也沒有調(diào)用

checkForComodification方法,那么這些情況是為什么呢垃僚?另外集绰,為什么 fori 沒有

問題的刪除了第一個‘張三’,但是為什么后面不再刪除了谆棺?在 fori 的情況下栽燕,

當?shù)谝粋€元素被刪除時罕袋,集合本身元素位置發(fā)生變化,刪除完第一個‘張三’后:i = 1碍岔,但是

第二個‘張三’被移動到了 index = 0 的位置浴讯,所以下一次 get(i) 時,獲取到的是被移動到

index = 1 的‘趙四’第二個‘張三’蔼啦,因此被巧妙地跳過了榆纽。所以,當使用 fori 進行刪除時捏肢,

雖然可以進行刪除操作奈籽,但存在數(shù)據(jù)問題。我們接著來看增強 for鸵赫。

String[] arr = new String[]{"張三", "張三", "趙四", "王五"};
String[] var2 = arr;
int i = arr.length;

for(int var4 = 0; var4 < i; ++var4) {
    String s = var2[var4];
    System.out.println(s);
}

List<Integer> list = new ArrayList();

for(i = 0; i < 5; ++i) {
    list.add(i);
}

Iterator var7 = list.iterator();

while(var7.hasNext()) {
    Integer integer = (Integer)var7.next();
    if ("張三".equals(str)) {
        list.remove(str);
    }
}

增強for是一個語法糖衣屏。所以在編譯代碼時,它會被解析成基礎(chǔ)代碼辩棒。如果是數(shù)組使用增強for

則解析過后就是 fori 的形式狼忱。如果是實現(xiàn)了 Iterable 的集合,則會解析成 iterator

迭代器的形式遍歷集合盗温。所以藕赞,在之前 ArrayList 的增強 for 中,會被解析成 iterator

迭代器形式遍歷卖局。所以會調(diào)用 iterator斧蜕,hasNext 方法以及 next 方法。但是呢砚偶!

這里有一個我們非常需要注意的地方批销,刪除方法我們是調(diào)用的 ArrayList 的 remove 方法。

我們接下來看下 Itr 源碼染坯。(注意:泛型也是語法糖均芽,也會被解析成基礎(chǔ)代碼,但這里不做講解)

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

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

在 ArrayList 實現(xiàn)的 iterator 方法中单鹿,返回了內(nèi)部類 Itr 實例對象掀宋。Itr 則實現(xiàn)了

Iterator 接口,所以會重寫 hasNext仲锄,next 方法劲妙。在 hasNext 中判斷了指針是否還指向元素。

而在 next 方法中掉了用我們前面提到的報錯方法 checkForComodification儒喊,這個方法拋出了

ConcurrentModificationException 異常镣奋。而這個異常是根據(jù) modCount != expectedModCount

這個條件達成的。好怀愧,現(xiàn)在我們就要來聊聊這個一開始就提到的 modCount 了侨颈。

protected transient int modCount = 0;
// add 方法調(diào)用
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// remove 方法調(diào)用
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

首先 modCount 并不在 ArrayList 中定義的余赢,而是在 AbstractList 中定義的,其記錄的是

集合被修改的次數(shù)哈垢。所以妻柒,在 add,remove 等方法中會見到 modCount++ 這個操作温赔。在‘張三’

的例子增強for中蛤奢,創(chuàng)建 Itr 內(nèi)部類時 int expectedModCount=modCount,expectedModCount

這個內(nèi)部類成員變量已被賦值陶贼,因為做了四次 add啤贩、一次 remove 操作, 所以

expectedModCount = modCount = 5拜秧。當增強 for第一次遍歷時痹屹,checkForComodification

方法判斷成功,刪除第二個‘張三’(第一個‘張三’是 fori 中刪除)上面說過枉氮,增強 for 中調(diào)用的是

ArrayList的刪除方法志衍。所以這時modCount++變成了6,所以在下一次刪除時聊替,

checkForComodification 方法判斷失敗楼肪,所以報了 ConcurrentModificationException 異常。

那么我們來看正常的 iterator 迭代器遍歷惹悄。

List<String> list = new ArrayList<>();
list.add("張三");
list.add("張三");
list.add("趙四");
list.add("王五");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if ("張三".equals(next)) {
        iterator.remove();
        System.out.println("張三已被刪除1");
    }
}
// itr 的刪除源碼
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

在迭代器遍歷刪除時春叫,我們調(diào)用的時 Itr 內(nèi)部類的刪除方法,我們明顯看到了

expectedModCount = modCount泣港,這也就解釋了為什么迭代器遍歷不會報錯而增強 for 會報錯

最后暂殖,阿里守則中,也明確提到了当纱,不要在 foreach 循環(huán)里刪除元素呛每。若需刪除元素,

請使用 Iterator 方式坡氯。在并發(fā)操作時晨横,需對 Iterator 對象加鎖。

查找元素 -> 如果有序箫柳,可以二分查找提高效率

如果要正序查找一個元素颓遏,可以使用 indexOf 方法;如果要倒序查找一個元素滞时,可以使用

lastIndexOf 方法contains方法可以判斷 ArrayList 中是否包含某個元素,內(nèi)部調(diào)用indexOf方法

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

如果 ArrayList 中的元素是經(jīng)過排序的滤灯,就可以使用二分查找法坪稽,效率更快

ArrayList<String> copy = new ArrayList<>(alist);
copy.add("1");
copy.add("2");
copy.add("2");
copy.add("4");
Collections.sort(copy);
System.out.println(copy);

int index = Collections.binarySearch(copy, "b");
System.out.println(index);

更新元素

來看一下 set() 方法的源碼

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

更新元素時曼玩,先對指定的下標進行檢查,看是否越界窒百,然后替換新值并返回舊值


(????)??嗨黍判!如果這篇文章小小幫助到你,可以幫小弟我點個贊呀篙梢。點贊是對我最大的支持與認可顷帖,感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渤滞,一起剝皮案震驚了整個濱河市贬墩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妄呕,老刑警劉巖陶舞,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绪励,居然都是意外死亡肿孵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門疏魏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來停做,“玉大人,你說我怎么就攤上這事大莫◎入纾” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵葵硕,是天一觀的道長眉抬。 經(jīng)常有香客問我,道長懈凹,這世上最難降的妖魔是什么蜀变? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮介评,結(jié)果婚禮上库北,老公的妹妹穿的比我還像新娘。我一直安慰自己们陆,他們只是感情好寒瓦,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坪仇,像睡著了一般杂腰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椅文,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天喂很,我揣著相機與錄音惜颇,去河邊找鬼。 笑死少辣,一個胖子當著我的面吹牛凌摄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播漓帅,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锨亏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了忙干?” 一聲冷哼從身側(cè)響起器予,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豪直,沒想到半個月后劣摇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡弓乙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年末融,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暇韧。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡勾习,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出懈玻,到底是詐尸還是另有隱情巧婶,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布涂乌,位于F島的核電站艺栈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏湾盒。R本人自食惡果不足惜湿右,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罚勾。 院中可真熱鬧毅人,春花似錦、人聲如沸尖殃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽送丰。三九已至缔俄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牵现。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工铐懊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞎疼。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像壁畸,于是被迫代替她去往敵國和親贼急。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容