удалить если деталь реализации - PullRequest
9 голосов
/ 04 февраля 2020

У меня есть маленький вопрос о деталях реализации, который я не понимаю в ArrayList::removeIf. Я не думаю, что я могу просто сказать так, как есть, без каких-либо предварительных условий.

Таким образом: реализация в основном представляет собой объем remove, в отличие от ArrayList::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 должен быть приведен в "хорошее" состояние, означающее, что внутренний массив фактически изменится. Опять же, при каждом отдельном вызове remove будут внутренние вызовы System::arrayCopy.

На контрасте removeIf умнее. Поскольку он выполняет итерацию внутри, он может сделать вещи более оптимизированными. То, как это происходит, интересно.

Сначала вычисляются индексы, из которых элементы должны быть удалены. Для этого сначала вычисляется крошечный BitSet, массив значений long, где в каждом индексе находится значение 64 bit (long). Несколько значений 64 bit делают это BitSet. Чтобы установить значение с определенным смещением, сначала нужно найти индекс в массиве, а затем установить соответствующий бит. Это не очень сложно. Допустим, вы хотите установить биты 65 и 3. Сначала нам нужен long [] l = new long[2] (потому что мы вышли за пределы 64 бит, но не более 128):

|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 здесь. В каком угловом случае я здесь скучаю?

1 Ответ

6 голосов
/ 04 февраля 2020

Вы смотрите на конкретный c (общий) случай, когда список, который вы называете 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 вызовет shiftTailOverGap с аргументом end, который может отличаться от size при применении к subList.

Аналогичная ситуация возникает при вызове clear(). В этом случае фактическая операция, выполняемая при вызове ее на самом ArrayList, настолько тривиальна, что она даже не вызывает метод shiftTailOverGap. Только при использовании чего-то вроде l.subList(a, b).clear() это закончится на removeRange(a, b) на l, что, в свою очередь, как вы уже выяснили сами, вызовет shiftTailOverGap(elementData, a, b) с b, который может быть меньше size.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...