ArrayListの機能とソースコードの読み取り

ArrayListは、日常の開発で最も頻繁に使用されるコレクションであり、高速アクセスのために好まれています。

一つ、工法

一般的に使用される構築方法は2つあり、1つはデフォルトのパラメーターなしの構築方法であり、もう1つはパラメーターを含む構築方法です。

デフォルトのパラメータなしの構築

//new ArrayList(0) 会创建一个空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {
    
    };

//new ArrayList()默认构造会创建一个提供默认大小的实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };

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

では、なぜEMPTY_ELEMENTDATAとDEFAULTCAPACITY_EMPTY_ELEMENTDATAの2つのインスタンスがあるのでしょうか。主な用途は、を展開するときに現在のArrayListが作成される構築方法識別することです。

パラメータ構造

/**
 * @param  initialCapacity  初始容量
 * 如果initialCapacity<0 就抛出异常
 */
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);
    }
}

一般に、ArrayListの3つの構造は次のとおりです。

  • new ArrList():UseDEFAULTCAPACITY_EMPTY_ELEMENTDATA(最初の追加要素を展開する必要があります)、デフォルトの長さは10です
  • new ArrList(0):UseEMPTY_ELEMENTDATA、(最初の追加要素を展開する必要があります)、ただし、最初の拡張の結果は上記とは異なります
  • new ArrList(21):長さ21の配列を作成します
  • その他の状況:例外をスローする

2つ、メンバー変数

特定の操作を確認する前に、関連するメンバー変数を確認してください

//new ArrayList() 第一次扩容后的大小
private static final int DEFAULT_CAPACITY = 10;

//elementData.lentgh是当前ArrayList的容量大小,用于扩容判断
transient Object[] elementData;

//当前ArrayList中的元素个数
private int size;

3、追加()操作

add()は、nullを含むすべての要素を許可します。

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 static int calculateCapacity(Object[] elementData, int minCapacity) {
    
    
	 //new ArrayList()在第一次add会return max(10,1)
     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
        return Math.max(DEFAULT_CAPACITY, minCapacity);
     }
     //new ArrayList(0) 在第一次add时会走这一步 return 1
     return minCapacity;
}

/*
    new ArrayList()会在grow()内扩容成 10
    new ArrayList(0)会在grow()内扩容成 1 
*/
private void ensureExplicitCapacity(int minCapacity) {
    
    
     modCount++;

     // new ArrayList()和new ArrayList(0)的length均为0,因为一开始创建的都是空的数组实例
     if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

        追加のプロセスは、最初に展開するかどうかを決定し、次に新しい要素を配列の最後に配置することです。
        new ArrayList()とnew ArrayList(0)は展開結果が異なることに注意してください。初めて追加するとき、calculateCapacity(Object[] elementData, int minCapacity)メソッドでnew ArrayList()は10を返し、new ArrayList(0)は1を返します。 。それらの戻り結果は、grow()の最初の展開の長さを決定します

private void grow(int minCapacity) {
    
    
        // new ArrayList()和new ArrayList(0)的length均为0,因为一开始创建的都是空的数组实例
        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);
}

グロー()メソッドへの配列の長さを拡張し1.5倍の長さ、および使用は、(oldCapacity >> 1)ビット単位操作を高効率で。の場合newCapacity >Integer.MAX_VALUE - 8、拡張された容量を直接Integer.MAX_VALUEに等しくすると、配列要素の数がInteger.MAX_VALUE-8以上になり、最大値に拡張されるのはなぜか疑問に思う人もいるかもしれません。この値が小さい場合は、多くの配列になります。」「より簡単に」最大に拡張し、メモリを浪費します。新しいアレイの
        容量を決定した後Arrays.copyOf(elementData, newCapacity)、元の参照が新しいアレイを指すように、新しいアレイが作成されます。
具体的なプロセスは次のとおりです。
ここに画像の説明を挿入

add(int index、E element):特定の位置に挿入

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

        ArrayListが挿入と変更に適していないのはなぜですか(インタビューテストサイト)?その理由は、挿入System.arraycopy(elementData, index, elementData, index + 1, size - index);すると、要素がコピーされた後にロケーションインデックスに挿入され、最初のインデックス+ 1から順番に配置され、要素が1つずつ戻って、最後に指定されたインデックス割り当て。このプロセスはまだ非常に時間がかかり、拡張された場合はさらに面倒になります。

addAll():コレクションのコンテンツをArrayListに追加します

//把集合的内容加到末尾
public boolean addAll(Collection<? extends E> c) {
    
    
    Object[] a = c.toArray();
    int numNew = a.length;
    //判断是否需要扩容
    ensureCapacityInternal(size + numNew); 
    //其中的参数size就是说把 来源数组a copy到目标数组elementData的最后一个位置
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

//把集合的内容添加到数组的某个位置
public boolean addAll(int index, Collection<? extends E> c) {
    
    
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0)
    	//先把目标数组elementData的部分元素后移numNew个单位
        System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
	
	//再把 来源数组a copy到目标数组elementData下标为[index,index+numNew]的位置
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

これらはすべてaddAll()ですが在指定位置插入 addAll(int index, Collection<? extends E> c)、ロジックはもう少し複雑で、2つのステップがあります。

  1. ターゲット配列の要素(インデックス位置から始まる要素)をnumNew単位だけ後方に移動します。ここで、numNewはソース配列の長さです。
  2. ソース配列aのすべての要素をコピーし、ターゲット配列elementDataの[index、index + numNew]の位置に配置します。

4、remove()

remove()は、削除する要素の後ろにあるすべての要素を1つずつ前方に移動して、削除操作を完了します。

remove(int index):指定された位置にある要素を削除します

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

指定した位置の要素を削除する場合remove(int index)の操作は次のとおりです。

  1. インデックスが範囲外かどうかを確認します
  2. インデックスが配列の最後にあるかどうかを判断します。配列の最後にある場合は、最後の要素をNullにします。elementData[--size] = null
  3. indexが配列の最後の要素でない場合は、elementData [index + 1]以降の要素をコピーし、添え字indexから順番に配置して、最後に配列の最後の要素をnullにします。

remove(Object o):指定された要素を削除します

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

指定された要素を削除remove(Object o)すると、配列をトラバースして1つずつ比較する必要があるため、少し面倒です。比較が等しい場合、呼び出しfastRemove(int index) 本质还是remove(int index)は次の要素を1つ進めます。

5 get():指定された位置にある要素を取得します

public E get(int index) {
    
    
    rangeCheck(index);
    return elementData(index);
}

ArrayListの最下層は配列であり、配列はランダムアクセスをサポートしています。メモリの継続性また、アレイの走査も効率的になります。二重リンクリストに基づくLinkedListは、1つずつトラバースする必要があります。メモリが連続していない理由は検索には適していません。

6、イテレータ

イテレータはArrayListの内部クラスであり、同じArrayListインスタンスで動作する複数のスレッドによって引き起こされるデータ整合性の問題(ダーティリード)を防ぐために、トラバーサル中に同時に削除または追加することはできません。

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;

    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;
        //new ArrayList(0)后调用next()会抛异常
        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();
    }
}

3つのメンバー変数があります。

  1. カーソル:次にアクセスする要素
  2. lastRet:最後にアクセスされた要素のインデックス。-1として定義され、配列が空かどうかを判断するために使用できます。
  3. expectedModCount:ArrayListの変更時間の期待値。modCountの期待値と実際の値が一致しない場合、イテレーターはトラバーサルプロセス中に(削除と追加のたびに)削除と追加で例外をスローすることを意味します。ArrayListの元の構造を変更すると、modCount + 1になります)。

イテレータには、hasNext()、next()、remove()の3つのメソッドがあります。

  1. 次の要素があるかどうかを確認するにはpublic boolean hasNext():(上付き文字にアクセスするために次の要素にカーソル合わせるときの代表)同じサイズで、現在が配列の最後の要素に到達したことを表します。
  2. 次の要素を返しますpublic E next()。配列が空の場合、NoSuchElementExceptionがスローされます。配列がnew ArrayList(0)によって構築されている場合、next()を直接呼び出すとConcurrentModificationExceptionがスローされます。通常、次に訪問する要素が返されます
  3. 要素のremove()削除:elementData [lastRet]の削除。次の要素を1桁前方に移動した後、カーソルも1桁前方に移動する必要がありますcursor = lastRetこれはに反映されます。

セブン、スレッドは安全ではありません

        ArrayListはスレッドセーフではありません。複数のスレッドが同じArrayListを同時にトラバース、追加、削除すると、ダーティリード、繰り返しの拡張、無駄なスペースなどの問題が発生することを想像してみてください。
        回避リスクに対するためには、ArrayListのではなく、同時変更を危険にさらすのフェイルファスト機構を備えている。その中で具体化されたif (modCount != expectedModCount) throw new ConcurrentModificationException();
        スレッドの安全性が要求される場合、ベクトルをすることができますCollections.synchronizedList使用、およびArrayListのはまたすることができますArrayListのをラップするために使用される。両方がされている保証同期のキーワード修正方法。スレッドの安全性。

おすすめ

転載: blog.csdn.net/qq_44384533/article/details/108498376