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.