Mam małe szczegółowe pytanie dotyczące implementacji, w którym nie rozumiem ArrayList::removeIf
. Nie sądzę, że mogę po prostu to po prostu przedstawić, tak jak jest, bez pewnych warunków wstępnych.
Jako taki: wdrożenie jest w zasadzie masowe remove
, w przeciwieństwie do ArrayList::remove
. Przykład powinien znacznie ułatwić zrozumienie. Powiedzmy, że mam tę listę:
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);
I chciałbym usunąć każdy element, który jest parzysty. Mógłbym zrobić:
Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
int elem = iter.next();
if (elem % 2 == 0) {
iter.remove();
}
}
Lub :
list.removeIf(x -> x % 2 == 0);
Wynik będzie taki sam, ale wdrożenie jest bardzo różne. Ponieważ iterator
jest to widok za ArrayList
każdym razem, gdy dzwonię remove
, podstawa ArrayList
musi być doprowadzona do „dobrego” stanu, co oznacza, że wewnętrzny układ faktycznie się zmieni. Ponownie, przy każdym pojedynczym wywołaniu remove
będą połączenia System::arrayCopy
wewnętrzne.
W przeciwieństwie do tego removeIf
jest mądrzejszy. Ponieważ wykonuje iterację wewnętrznie, może zoptymalizować rzeczy. Sposób, w jaki to robi, jest interesujący.
Najpierw oblicza indeksy, z których elementy powinny zostać usunięte. Odbywa się to poprzez obliczenie najpierw małej BitSet
, tablicy long
wartości, gdzie przy każdym indeksie znajduje się 64 bit
wartość (a long
). Wiele 64 bit
wartości sprawia, że jest to a BitSet
. Aby ustawić wartość dla określonego przesunięcia, najpierw musisz znaleźć indeks w tablicy, a następnie ustawić odpowiedni bit. To nie jest bardzo skomplikowane. Powiedzmy, że chcesz ustawić bit 65 i 3. Najpierw potrzebujemy long [] l = new long[2]
(ponieważ przekroczyliśmy 64 bity, ale nie więcej niż 128):
|0...(60 more bits here)...000|0...(60 more bits here)...000|
Najpierw znajdziesz indeks: 65 / 64
(faktycznie tak robią 65 >> 6
), a następnie w tym indeksie ( 1
) umieść potrzebny bit:
1L << 65 // this will "jump" the first 64 bits, so this will actually become 00000...10.
To samo dotyczy 3
. W ten sposób długa tablica stanie się:
|0...(60 more bits here)...010|0...(60 more bits here)...1000|
W kodzie źródłowym nazywają to BitSet - deathRow
(ładna nazwa!).
Weźmy ten even
przykład tutaj, gdzielist = 2, 4, 6, 5, 5
- iterują tablicę i obliczają to
deathRow
(gdziePredicate::test
jesttrue
).
deathRow = 7 (000 ... 111)
czyli indeksy = [0, 1, 2] należy usunąć
- teraz zastępują elementy w podstawowej tablicy na podstawie tego deathRow (nie wchodząc w szczegóły, jak to się robi)
tablica wewnętrzna staje się: [5, 5, 6, 5, 5]. Zasadniczo poruszają elementy, które powinny pozostać przed tablicą.
Mogę wreszcie zadać pytanie.
W tym momencie wiedzą:
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
Dla mnie jest tutaj jeden krok:
void getRidOfElementsFromWToEnd() {
for(int i=w; i<end; ++i){
es[i] = null;
}
size = w;
}
Zamiast tego dzieje się tak:
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;
}
Celowo zmieniłem nazwę zmiennych.
Jaki jest sens dzwonienia:
System.arraycopy(es, end, es, w, size - end);
Zwłaszcza size - end
, że end
jest size
cały czas - nigdy się nie zmienia (więc zawsze tak jest zero
). Jest to w zasadzie NO-OP tutaj. Jakiego narożnika tu brakuje?
System.arraycopy(es, end, es, w, size - end)
jako podstawowych szczegółów wdrożeniaremoveIf
? Niemal poczułem, że czytam odpowiedź na jakieś inne pytanie pomiędzy. (Czytając powyższy komentarz) Wydaje mi się, że ostatecznie zadało to trywialne pytanie. Czy tak jestSystem.arrayCopy
. Niemniej jednak była to zabawna podróż przez szczegóły (ten wewnętrzny zestaw bitów okazuje się mieć taki sam pomysł jakjava.util.BitSet
)range
...), a ja ją zaakceptuję.java.util.BitSet
. Dla mnie ponowna realizacjaBitSet
operacji nie wygląda znacznie lepiej niż oryginał. Nie udało się pominąć całych słów.Odpowiedzi:
Patrzysz na konkretny (powszechny) przypadek, w którym wywoływana lista
removeIf
jest taka sama jakArrayList
. Tylko w tym przypadku możesz założyć, żeend
zawsze jest równasize
.Przykładem może być:
Podobnie,
removeAll
zadzwonishiftTailOverGap
zend
argumentem, który może różnić się odsize
zastosowania w przypadkusubList
.Podobna sytuacja powstaje podczas połączenia
clear()
. W takim przypadku faktyczna operacja wykonywana podczas wywoływania goArrayList
sama jest tak trywialna, że nawet nie wywołujeshiftTailOverGap
metody. Tylko przy użyciu coś takiegol.subList(a, b).clear()
, będzie to skończyć wremoveRange(a, b)
sprawiel
, co z kolei, jak już okazało się, siebie, invokeshiftTailOverGap(elementData, a, b)
zb
którego może być mniejsza niżsize
.źródło