ArrayList分析1-循環、擴容、版本

ArrayList分析1-循環、擴容、版本

轉載請註明出處 https://www.cnblogs.com/funnyzpc/p/16407733.html

前段時間抽空看了下ArrayList的源碼,發現了一些有意思的東東,真的是大受裨益哈,尤其是版本問題😊
所以,本篇博客開始我將大概分三篇講講ArrayList裏面一些有意思的點哈,由於源碼大概一千八百逾行,裏面大多代碼都很通俗,也有些部分存在重複的(Itr以及SubList的內部方法),因爲大多通俗遂這裏不會逐行的分析哈,好了,現在開始~😂

一.關於循環的一個問題

首先,我給出一個很easy的循環

    public static void main(String[] args) {
      for(int i = 0;i<8;i++){
          System.out.print(i+"\t"); // 0	1	2	3	4	5	6	7	
      }
  }

看起來很簡單吧,哈哈,這時我會問:各位有沒試過將i提到for循環外邊呢,像下面這樣:

    public static void main(String[] args) {
      int i;
      for(i = 0;i<8;i++){
          System.out.println(i);
      }
      System.out.println(i);// ?
  }

上面第六行的i會輸出什麼呢?真是個有意思的問題,這真是一個微小而有意思的問題,我們經常使用,卻很少利用for去做一些別樣的事兒,ArrayList就有一騷操作,
原本我是準備臆測出來,卻發現怎麼也理解不了,當然啦,這個問題接下來我會說到:探究這個問題我們先看看一個普通的for循環的結構

for(定義1;定義2;定義3){
  //定義4 :循環內的語句塊
}

個人文采拙劣,這裏就用定義一詞哈😳
定義1: 這個地方我們經常會用int i=0; 這樣一個語句,其實這個地方是對循環的變量做一次定義,這個地方的定義是一次性的,而且是第一次循環的時候會執行。
定義2: 這裏一般是個判斷性的表達式,而且這個地方的整體必須返回一個boolean,這個很重要,既然這個地方只需要返回一個布爾的結果,那麼你想過沒有,如果這個地方 我寫的是 (i<10 && i>=0) 會不會拋錯呢 🤣
定義4: 這是循環內語句塊,通常我們會取到當前循環到的i進行某些邏輯處理,這裏不是重點哈。
定義3: 這個地方是重點,一般我們會說每次循環後我們會將i--或者i++, 這種循環變量變化我們一般都會寫在這個位置,這是_very very normal_的,但問題是每次執行完定義4的部分 就一定會執行定義3這個地方嘛? 答案是:一定會的!,爲什麼呢,看看生成的字節碼指令就知道了哈🌹 :

0 iconst_0
1 istore_1
2 iload_1
3 bipush 8
5 if_icmpge 21 (+16)
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i
15 iinc 1 by 1
18 goto 2 (-16)
21 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
24 iload_1
25 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i
28 return

以上是main函數內的完整字節碼內容(jdk=java8), 可以看到指令內有兩處println,自然第一個println即是for循環內的(標號12處的),下面一行就很重要了,官方描述是:將局部棧幀的索引+1(see: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc),說明白些也就是將i加一,然後就到了標號18這個位置,goto是將當前語句指向標號2這個位置 將storei加載...到這裏也就很明白了, goto指令是在i自增1之後,可以完全確認循環外的println打印的就一定是 8
看似簡單的操作 ArrayList 則時常使用,比如可以用i循環,循環完成後,數組的大小不就是這個i了?以下ArrayList->Itr內的一段代碼:

        // 循環每個剩餘操作
      // 這是java8提供給iterator的函數式循環接口,其使用方式如下
      //        ArrayList arr = new ArrayList();
      //        arr.add("a");
      //        arr.add("b");
      //        arr.add("c");
      //        System.out.println(arr);
      //        Iterator iterator = arr.iterator();
      //        iterator.next(); // a
      //        iterator.forEachRemaining(item-> System.out.println(item)); // b c
      @Override
      @SuppressWarnings("unchecked")
      public void forEachRemaining(Consumer<? super E> consumer) {
          // 檢查是否爲null,否則拋出錯誤
          Objects.requireNonNull(consumer);
          // 獲取當前數組大小並檢查迭代器的遊標位置是否大於數組大小
          final int size = ArrayList.this.size;
          int i = cursor;
          if (i >= size) {
              return;
          }
          // 老實說 elementData.length 與 ArrayList.this.size 是一對一關聯的,這裏這樣做似乎多餘
          final Object[] elementData = ArrayList.this.elementData;
          if (i >= elementData.length) {
              throw new ConcurrentModificationException();
          }
          while (i != size && modCount == expectedModCount) {
              // 消費這個元素,同時將遊標位置+1
              consumer.accept((E) elementData[i++]);
          }
          // update once at end of iteration to reduce heap write traffic 在迭代結束時更新一次以減少堆寫入流量
          // 因爲i在以上已經+1了,所以這裏直接賦值以及重置當前迭代的索引位置(lastRet)
          cursor = i;
          lastRet = i - 1;
          checkForComodification();
      }

上面這是迭代器內的部分迭代方法的定義,可以看到有句 cursor = i; 這個地方的i其實也就是size哈,稍不注意就理解錯了,小問題讓大家見笑了...😂

二.ArrayList擴容

ArrayList內部其實也就是維護了一個 Object 類型的數組,它具體是這樣定義的transient Object[] elementData; ,看起來是不是超簡單呢?呵呵呵,如果將ArrayList看作成一個大水缸的話,這個elementData就是水缸的本體,ArrayList可以看做一個用戶態的接口,簡單理解它其實就是是水缸外層刷的油漆或蓋子抑或是漏斗。
打比方,如果這個水缸 (elementData )能裝四桶水,那麼這四桶水我們用 initialCapacity 這個變量來表示,當前實際上如果只有兩桶水,則這個水缸實際儲水容量(兩桶水)我們用size來表示,這樣就好理解了吧?
好了,如果預先知道將有8桶水倒入缸內,那我們就要準備一個能容納8桶水的水缸,對於代碼就是這樣的:public ArrayList(int initialCapacity) {....}
當然,如果我們只允許用一個水缸來儲存水的話,這個水缸當前如果是滿載(8桶水),第9桶水的時候就需要準備一個至少能容納9桶水的水缸,對於ArrayList來說這時候就需要擴容了,代碼是這樣的:

    // 確保顯式容量(官方直譯,不懂直接看代碼)
   private void ensureExplicitCapacity(int minCapacity) {
       // 這個變量記錄的是當前活動數組被修改的次數
       // 每添加一個(準確的說是一次)元素修改次數+1,如果是addAll也算做是+1
       modCount++;
       // overflow-conscious code 判斷是否溢出
       // 可以簡單的理解 minCapacity 爲當前需要保證的最小容量(具體大小爲當前容量+1:這是對於當前add元素的個數而定的),elementData.length則爲當前活動數組的容量
       // minCapacity 也爲添加元素後所需數組容量大小,如果(所需容量)大於當前(添加前)數組容量即需要<b>擴容</b>
       if (minCapacity - elementData.length > 0)
           grow(minCapacity); //增長(擴容)
   }

當然這時本着少騰挪多儲水的原則,我們一般不會準備一個只能容納9桶水的水缸,水缸太大也不好,容易浪費缸的容量維護也麻煩😓,所以對於ArrayList這個水缸,我們每次增長爲現有容量的1.5倍(多了50%左右,如果當前Capacity是10->增長到15,9->增長到13),具體對應到ArrayList的代碼就是:

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     * 增加容量以確保它至少可以容納最小容量參數指定的元素數量。
     * @param minCapacity the desired minimum capacity 所需的最小容量(也即當前需要的容量大小)
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 這裏的增長策略是 oldCapacity=10 -> newCapacity=15 oldCapacity=9 -> newCapacity=14
        // 即 每一次增長的爲上一次的一半
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 這裏個人覺得只是一個保險,對於類似addAll這樣的操作 newCapacity 可能小於一次add的數量
        // 比如當前容量是10[oldCapacity:10->newCapacity:15],addAll(100)後所需的容量還是不夠 這時就會出現[newCapacity:100]
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 這裏也是一個保險,對於待擴容後的大小比數組最大(MAX_ARRAY_SIZE)還要大的時候啓用hugeCapacity(minCapacity)
        // 這裏調用 hugeCapacity 後頂多擴容8個大小 MAX_ARRAY_SIZE=2147483639(Integer.MAX_VALUE-8)
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        // 翻譯: minCapacity 通常接近 size,所以這是一個勝利
        // Arrays.copyOf:
        //  複製指定的數組,截斷或填充空值(如有必要),使副本具有指定的長度。對於在原始數組和副本中都有效的所有索引,
        //  這兩個數組將包含相同的值。對於在副本中有效但在原始副本中無效的任何索引,副本將包含 null。
        //  當且僅當指定長度大於原始數組的長度時,此類索引才會存在。結果數組與原始數組的類完全相同。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

上面有一句很重要 : int newCapacity = oldCapacity + (oldCapacity >> 1); ,每次換用更大的水缸時都會將之前缸內的水兌過來,對應ArrayList就是這句:elementData = Arrays.copyOf(elementData, newCapacity);
算是很形象吧😅,這只是簡單的理解,擴容也還有很多內容,比如什麼時候擴容,擴容一個單位不夠怎麼辦,滿了怎麼辦???等等問題....

三.ArrayList中的版本管理

一開始大家會覺得這是個奇怪的問題,ArrayList中爲啥會有版本,版本做什麼用?

首先,我詳細解答第一個問題:ArrayList中爲什麼有版本?,首先先看一段ArrayList的源碼,關於Iterator的:
(以下爲第一段代碼)

    public Iterator<E> iterator() {
      // 雖然與都會返回一個迭代器,但是iterator只能單向循環,且不能實現增刪改查
      // 詳見: https://www.cnblogs.com/tjudzj/p/4459443.html
      return new Itr();
  }

(以下爲第二段代碼)

       public E next() {
          // 版本檢查
          checkForComodification();
          // 這個將遊標賦予i,然後檢查是否i是否超出當前數組索引位置(size)
          // 我暫時沒看出以下三行跟 hasNext() 有多少區別。。。,而且checkForComodification內也是做了安全檢查了的
          // 總結就是:十分沒必要啊...
          int i = cursor;
          if (i >= size)
              throw new NoSuchElementException();
          // 不大明白爲啥要再整個引用 ,通過這個新引用索引返回數組值
          Object[] elementData = ArrayList.this.elementData;
          if (i >= elementData.length)
              throw new ConcurrentModificationException();
          // 因爲迭代器每次循環前都會調用 hasNext ,故此推測這裏也應該將遊標+1
          cursor = i + 1;
          // 需要說明的是這個 lastRet 是個成員變量,而i只是個方法內臨時變量而已
          // 所以每循環一次這個 lastRet 需要記錄爲當前返值前的當前索引位置
          return (E) elementData[lastRet = i];
      }

(以下爲第三段代碼)

      //// example:
      //  ArrayList arr = new ArrayList();
      //  arr.add("a");
      //  arr.add("b");
      //  arr.add("c");
      //  ListIterator listIterator = arr.listIterator();
      //  arr.remove("a");// throw error
      //  Object previous = listIterator.previous();
      final void checkForComodification() {
          if (modCount != expectedModCount)
              throw new ConcurrentModificationException();
      }

額,由於只是截取了部分代碼,我先簡單講講上面三段代碼,第一段代碼很明顯,我們使用迭代器的時候首先調用的就是集合類的 iterator() 方法,而 iterator() 內部 只做了一件事兒:new Itr() ,現在知道迭代器是一個類對象,這點很重要。
繼續看第二段next()方法,這個方法內部第一行代碼 是 checkForComodification(), 這是一個較爲特殊的存在,點進去會看到以上第三段代碼,if判斷內有兩個參數 ,一個是 modCount (直譯爲修改次數),第二個參數爲 expectedModCount (預期修改的次數),點到這個參數定義,它只有一句很簡單的定義int expectedModCount = modCount; ,是不是很迷糊😂?
next()內還有一句也很重要 Object[] elementData = ArrayList.this.elementData; ,這句估計很好懂了,Itr迭代器內使用的數組其實也就是ArrayList中維護的數組對象(elementData),倒退一步,再往回思考下 checkForComodification()看...
不知讀者老爺有沒恍然大悟,其實很簡單啦: Itr對象不希望你在使用Itr迭代器的過程中修改(主要是增刪)ArrayList中的(elementData)元素,不然在迭代的時候源數組少了個元素會直接拋錯的,Itr內的expectedModCount只會在 new Itr() 時被賦值一次,這就是很好的證明啦~
ItrIterator的實現,裏面只有迭代的操作,如果有更復雜的操作,比如ListItr(是Itr以及ListIterator的繼承實現) 裏面更是對迭代器增加了增刪改查方法,以及SubList這個對象內部也是,內部均是對ArrayList維護elementData直接操作(他們並未拷貝elementData),所以裏面的增刪操作不僅僅要比較ArrayListelementData版本,也要在操作(增刪)之後同步ArrayListmodCount版本,以下是ListItr內的add(E e)函數的源碼:

public void add(E e) {
            checkForComodification();
            try {
                // ArrayList的添加方法,在遊標位置添加元素,遊標及之後的元素往後移動,
                int i = cursor;
                // 這裏還需要注意的是這個插入是在當前元素之後插入元素,ArrayList則是在元素之前,這主要是遊標是當前位置+1
                ArrayList.this.add(i, e);
                // 因爲增加了個元素,所以遊標的位置要+1,當前位置lastRet會在下一次調用next或previous時會被重置
                cursor = i + 1;
                lastRet = -1;
                // 這個其實類似於版本的概念,主要由於ArrayList與Iterator修改不同步,expectedModCount,這個會在 checkForComodification() 中進行校驗
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

以上代碼中有這樣一句 expectedModCount = modCount; 不知讀者們明白否。。。😂

ArrayList中爲啥有版本管理,版本管理怎麼用?

好了,我總結下本小節開頭的的兩個問題
首先版本管理就是在增刪元素的時候對 modCount 自增1
因爲對ArrayList的迭代器 ItrListItr以及SubList(截取類) 他們是單獨類對象同時內部也是直接操作的ArrayList的源elementData數組對象,所以在ArrayList添加元素時這三個類內部方法均不知道數組元素個數已發生變化,所以在操作elementData時候均需要判讀版本是否一致,這就是爲啥有版本;
他解決的是:這幾個類在操作 elementData (ArrayList的)時 ArrayList可能對其的增刪導致的版本不一致的問題,總結似乎臭長了些,但就是這麼個意思😂
理解這個很重要,不然你在讀 ArrayListaddremove這類方法時不知modCount作甚云云,哈哈。。。

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