removeIf実装の詳細


9

で理解できない小さな実装の詳細な質問がありArrayList::removeIfます。最初にいくつかの前提条件がないと、単純にそれを置くことができないと思います。

そのため:とは異なり、実装は基本的にバルク です。例は物事を非常に理解しやすくするはずです。私がこのリストを持っているとしましょう:removeArrayList::remove

List<Integer> list = new ArrayList<>(); // 2, 4, 6, 5, 5
list.add(2);
list.add(4);
list.add(6);
list.add(5);
list.add(5); 

そして、私は偶数であるすべての要素を削除したいと思います。私はそれをできた:

Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
    int elem = iter.next();
    if (elem % 2 == 0) {
         iter.remove();
    }
}

または

list.removeIf(x -> x % 2 == 0);

結果は同じですが、実装は大きく異なります。はのiteratorビューなので、ArrayListを呼び出すたびremoveに、基になるものArrayListを「良好」な状態にする必要があります。つまり、内部配列は実際に変更されます。この場合も、の呼び出しごとにremoveSystem::arrayCopy内部で呼び出しが行われます。

対照的removeIfにスマートです。内部で反復を行うため、物事をより最適化することができます。これを行う方法は興味深いです。

まず、要素が削除されるはずのインデックスを計算します。これは、最初に、各インデックスに値(a )が存在する値のBitSet配列であるtinyを計算することによって行われlongます。複数の値はこれをaにします。特定のオフセットに値を設定するには、まず配列のインデックスを見つけてから、対応するビットを設定する必要があります。これはそれほど複雑ではありません。ビット65と3を設定するとします。最初に、次のaが必要です(64ビットを超えたが、128ビットを超えていないため)。64 bitlong64 bitBitSetlong [] l = new long[2]

|0...(60 more bits here)...000|0...(60 more bits here)...000|

あなたは最初にインデックスを見つけます:(65 / 64実際にあります65 >> 6)次に、そのインデックス(1)に必要なビットを入れます:

1L << 65 // this will "jump" the first 64 bits, so this will actually become 00000...10. 

同じこと3。そのため、長い配列は次のようになります。

|0...(60 more bits here)...010|0...(60 more bits here)...1000|

ソースコードでは、これをBitSet- deathRow(いい名前!)と呼んでいます。


evenここでその例を見てみましょう。list = 2, 4, 6, 5, 5

  • 彼らは配列を繰り返し、これを計算しますdeathRow(はPredicate::testですtrue)。

deathRow = 7(000 ... 111)

インデックス= [0、1、2]が削除されることを意味します

  • それらは、そのdeathRowに基づいて、基になる配列の要素を置き換えます(これがどのように行われるかについては詳しく説明しません)

内部配列は:[5、5、6、5、5]になります。基本的に、それらは配列の前に残るはずの要素を移動します。


ようやく質問を持ち込めます。

この時点で、彼らは次のことを知っています。

 w   ->  number of elements that have to remain in the list (2)
 es  ->  the array itself ([5, 5, 6, 5, 5])
 end ->  equal to size, never changed

私には、ここで実行する単一のステップがあります:

void getRidOfElementsFromWToEnd() {
    for(int i=w; i<end; ++i){
       es[i] = null;
    }
    size = w;
}

代わりに、これは起こります:

private void shiftTailOverGap(Object[] es, int w, int end) {
    System.arraycopy(es, end, es, w, size - end);
    for (int to = size, i = (size -= end - w); i < to; i++)
        es[i] = null;
}

ここでは、意図的に変数の名前を変更しました。

呼び出しのポイントは何ですか:

 System.arraycopy(es, end, es, w, size - end);

特にsize - end、以降はend ある sizeすべての時間-それは変更されることはありません(これは常にあるのでzero)。これは基本的にここではNO-OPです。ここではどのコーナーケースが欠けていますか?


2
私はこれらの詳細を理解するのに1日半だけ無駄にしましたが、これは明らかであり、この方法は他の場所でも使用されています。私はばかです:|
ユージーン

正直なところ、あなたは私を混乱させた。System.arraycopy(es, end, es, w, size - end)の基礎となる実装の詳細としてのの使用に関する質問はありましたremoveIfか?まるで他の質問への答えを読んでいるような気がしました。(上記のコメントを読んで)最終的には簡単な質問に終わったと思います。そうですか?
ナマン

@Naman正確には、それは恐ろしいことでしたSystem.arrayCopy。それにもかかわらず、詳細をたどる楽しい旅でした(その内部ビットセットはと同じ考えを持つことが判明しましたjava.util.BitSet
ユージーン

@Naman必要な場合は、それがNOOP ではない(ヒント:range...)でない場合の回答を提供でき、それを受け入れます。
ユージーン

1
Java 8では@Eugeneが使用しますjava.util.BitSet。私にとって、操作の再実装は、元のBitSet操作よりもはるかに良く見えません。単語全体をスキップする機会がありませんでした。
Holger

回答:


6

あなたが要求するリストremoveIfがと同じである特定の(一般的な)ケースを見ていArrayListます。この場合のみ、endは常にと等しいと想定できますsize

反例は次のようになります:

ArrayList<Integer> l = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7));
l.subList(2, 5).removeIf(i -> i%2 == 1);

同様に、に適用される場合とは異なる引数を使用してremoveAll呼び出します。shiftTailOverGapendsizesubList

を呼び出すと、同様の状況が発生しますclear()。その場合、ArrayListそれ自体を呼び出すときに実行される実際の操作は非常に簡単なので、shiftTailOverGapメソッドを呼び出すことすらありません。以下のようなものを使用している場合にのみl.subList(a, b).clear()、それはで終わるだろうremoveRange(a, b)lすでに、呼び出し自分自身を見つけたとして、その意志今度は、shiftTailOverGap(elementData, a, b)bよりも小さくすることができるがsize

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.