removeIf szczegóły implementacji


9

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ż iteratorjest to widok za ArrayListkażdym razem, gdy dzwonię remove, podstawa ArrayListmusi być doprowadzona do „dobrego” stanu, co oznacza, że ​​wewnętrzny układ faktycznie się zmieni. Ponownie, przy każdym pojedynczym wywołaniu removebędą połączenia System::arrayCopywewnętrzne.

W przeciwieństwie do tego removeIfjest 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 longwartości, gdzie przy każdym indeksie znajduje się 64 bitwartość (a long). Wiele 64 bitwartoś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 evenprzykład tutaj, gdzielist = 2, 4, 6, 5, 5

  • iterują tablicę i obliczają to deathRow(gdzie Predicate::testjest true).

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?


2
Właśnie zmarnowałem 1/2 dnia na zrozumienie tych szczegółów, a to jest tak oczywiste, że ta metoda jest używana również w innych miejscach. Jestem idiotą: |
— Eugene

Szczerze mówiąc, wprawiłeś mnie w zakłopotanie. Czy Twoje pytanie dotyczyło wykorzystania System.arraycopy(es, end, es, w, size - end)jako podstawowych szczegółów wdrożenia removeIf? 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 jest
— Naman

@Naman dokładnie, chodziło o to, czego się obawiałem System.arrayCopy. Niemniej jednak była to zabawna podróż przez szczegóły (ten wewnętrzny zestaw bitów okazuje się mieć taki sam pomysł jak java.util.BitSet)
— Eugene

@Naman, jeśli chcesz, możesz udzielić odpowiedzi tam, gdzie nie jest to NOOP (wskazówka: range...), a ja ją zaakceptuję.
— Eugene

1
@Eugene w Javie 8, używa java.util.BitSet. Dla mnie ponowna realizacja BitSetoperacji nie wygląda znacznie lepiej niż oryginał. Nie udało się pominąć całych słów.
— Holger

Odpowiedzi:


6

Patrzysz na konkretny (powszechny) przypadek, w którym wywoływana lista removeIfjest taka sama jak ArrayList. Tylko w tym przypadku możesz założyć, że endzawsze jest równa size.

Przykładem może być:

ArrayList<Integer> l = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7));
l.subList(2, 5).removeIf(i -> i%2 == 1);

Podobnie, removeAllzadzwoni shiftTailOverGapz endargumentem, który może różnić się od sizezastosowania w przypadku subList.

Podobna sytuacja powstaje podczas połączenia clear(). W takim przypadku faktyczna operacja wykonywana podczas wywoływania go ArrayListsama jest tak trywialna, że ​​nawet nie wywołuje shiftTailOverGapmetody. Tylko przy użyciu coś takiego l.subList(a, b).clear(), będzie to skończyć w removeRange(a, b)sprawie l, co z kolei, jak już okazało się, siebie, invoke shiftTailOverGap(elementData, a, b)z bktórego może być mniejsza niż size.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.