RemoveIf деталі реалізації


9

У мене є невелике питання щодо детальної реалізації, яке я не розумію 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значення (a long). Кілька 64 bitзначень роблять це a 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 тут. Якого кутового корпусу я тут пропускаю?


2
Я щойно витрачав 1/2 на день, розуміючи ці деталі, і це так очевидно, що цей метод використовується і в інших місцях. Я ідіот: |
— Євген

Чесно кажучи, ти залишив мене розгубленим. Чи було ваше запитання щодо використання System.arraycopy(es, end, es, w, size - end)в якості основної деталі впровадження removeIf? Я майже відчував, що я читав між собою відповідь на якесь інше запитання. (Читаючи коментар вище) Я відчуваю, що нарешті опинився у тривіальному питанні. Невже це так?
— Наман

@Naman точно, мова йшла про те, що воно страшно System.arrayCopy. Тим не менш, це було цікаве подорож до деталей (виявляється, що внутрішній набір бітів має таку саму ідею, як java.util.BitSet)
— Євген

@Naman, якщо ви хочете, ви можете надати відповідь, де це не NOOP (підказка: range...), і я прийму його.
— Євген

1
@Eugene в Java 8, він використовує java.util.BitSet. По-моєму, повторна реалізація BitSetоперацій не виглядає суттєво краще, ніж оригінал. Можливість пропустити цілі слова пропущена.
— Холгер

Відповіді:


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буде shiftTailOverGapвикликано endаргумент, який може відрізнятися від sizeзастосованого до a subList.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.