У мене є невелике питання щодо детальної реалізації, яке я не розумію 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 тут. Якого кутового корпусу я тут пропускаю?
System.arraycopy(es, end, es, w, size - end)
в якості основної деталі впровадження removeIf
? Я майже відчував, що я читав між собою відповідь на якесь інше запитання. (Читаючи коментар вище) Я відчуваю, що нарешті опинився у тривіальному питанні. Невже це так?
System.arrayCopy
. Тим не менш, це було цікаве подорож до деталей (виявляється, що внутрішній набір бітів має таку саму ідею, як java.util.BitSet
)
range
...), і я прийму його.
java.util.BitSet
. По-моєму, повторна реалізація BitSet
операцій не виглядає суттєво краще, ніж оригінал. Можливість пропустити цілі слова пропущена.