removeIf dettagli dell'implementazione


9

Ho una piccola domanda sui dettagli di implementazione che non riesco a capire ArrayList::removeIf. Non credo di poter semplicemente dirlo così com'è senza prima alcune condizioni preliminari.

Come tale: l'implementazione è sostanzialmente una massa remove , a differenza ArrayList::remove. Un esempio dovrebbe rendere le cose molto più facili da capire. Diciamo che ho questo elenco:

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); 

E vorrei rimuovere ogni elemento uniforme. Potrei fare:

Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
    int elem = iter.next();
    if (elem % 2 == 0) {
         iter.remove();
    }
}

Oppure :

list.removeIf(x -> x % 2 == 0);

Il risultato sarà lo stesso, ma l'implementazione è molto diversa. Dal momento che iteratorè una visione di ArrayList, ogni volta che chiamo remove, il sottostante ArrayListdeve essere portato in uno stato "buono", il che significa che l'array interno cambierà effettivamente. Ancora una volta, ad ogni singola chiamata di remove, ci saranno chiamate System::arrayCopyinternamente.

Al contrario removeIfè più intelligente. Dal momento che esegue l'iterazione internamente, può rendere le cose più ottimizzate. Il modo in cui lo fa è interessante.

Calcola innanzitutto gli indici da cui si suppone che gli elementi vengano rimossi. Questo viene fatto calcolando prima un valore minuscolo BitSet, una matrice di longvalori in cui in ciascun indice risiede un 64 bitvalore (a long). 64 bitValori multipli rendono questo un BitSet. Per impostare un valore su un determinato offset, è innanzitutto necessario individuare l'indice nell'array e quindi impostare il bit corrispondente. Questo non è molto complicato. Supponiamo che tu voglia impostare i bit 65 e 3. Innanzitutto abbiamo bisogno di un long [] l = new long[2](perché siamo andati oltre i 64 bit, ma non più di 128):

|0...(60 more bits here)...000|0...(60 more bits here)...000|

Prima trovi l'indice: 65 / 64(lo fanno effettivamente 65 >> 6) e poi in quell'indice ( 1) inserisci il bit necessario:

1L << 65 // this will "jump" the first 64 bits, so this will actually become 00000...10. 

Stessa cosa per 3. Di conseguenza, tale array lungo diventerà:

|0...(60 more bits here)...010|0...(60 more bits here)...1000|

Nel codice sorgente chiamano questo BitSet - deathRow(bel nome!).


Facciamo questo evenesempio qui, dovelist = 2, 4, 6, 5, 5

  • ripetono l'array e lo calcolano deathRow(dove si Predicate::testtrova true).

deathRow = 7 (000 ... 111)

significa che gli indici = [0, 1, 2] devono essere rimossi

  • ora sostituiscono gli elementi nell'array sottostante basato su quel deathRow (non entrando nei dettagli come questo è fatto)

l'array interno diventa: [5, 5, 6, 5, 5]. Fondamentalmente spostano gli elementi che dovrebbero rimanere di fronte alla matrice.


Posso finalmente portare la domanda.

A questo punto, sanno:

 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

Per me, c'è un solo passo da fare qui:

void getRidOfElementsFromWToEnd() {
    for(int i=w; i<end; ++i){
       es[i] = null;
    }
    size = w;
}

Invece, questo accade:

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;
}

Ho rinominato le variabili di proposito qui.

Qual è il punto nella chiamata:

 System.arraycopy(es, end, es, w, size - end);

Soprattutto size - end, dato che end è size tutto il tempo - non è mai cambiato (quindi questo è sempre zero). Questo è fondamentalmente un NO-OP qui. Quale caso d'angolo mi manca qui?


2
Ho appena sprecato 1/2 al giorno a capire questi dettagli, e questo è così ovvio, questo metodo viene utilizzato anche in altri luoghi. Sono un idiota: |
Eugene,

Onestamente, mi hai lasciato confuso. La tua domanda era sull'uso di System.arraycopy(es, end, es, w, size - end)come i dettagli di implementazione sottostanti di removeIf? Mi sembrava quasi di leggere una risposta a qualche altra domanda in mezzo. (Leggendo il commento sopra) Sento che alla fine è finita in una domanda banale. È così?
Naman,

@Naman esattamente, si trattava di quello temuto System.arrayCopy. Tuttavia è stato un viaggio divertente attraverso i dettagli (quel set di bit interno risulta avere la stessa idea di java.util.BitSet)
Eugene

@Naman, se vuoi, puoi fornire una risposta laddove non è un NOOP (suggerimento: range...) e lo accetterò.
Eugene,

1
@Eugene in Java 8, lo usa java.util.BitSet. Per me, la reimplementazione delle BitSetoperazioni non sembra significativamente migliore dell'originale. L'opportunità di saltare intere parole è stata persa.
Holger,

Risposte:


6

Stai osservando il caso specifico (comune) in cui l'elenco, su cui fai ricorso removeIf, è lo stesso del ArrayList. Solo in questo caso, puoi presumere che endsia sempre uguale a size.

Un controesempio sarebbe:

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

Allo stesso modo, removeAllchiamerà shiftTailOverGapcon un endargomento che può differire da sizequando viene applicato a subList.

Una situazione simile si presenta quando si chiama clear(). In tal caso, l'operazione effettiva, eseguita quando la si chiama su ArrayListse stessa, è così banale che non chiama nemmeno il shiftTailOverGapmetodo. Solo quando si utilizza qualcosa di simile l.subList(a, b).clear(), ma finirà in removeRange(a, b)su l, che a sua volta, come già scoperto da soli, invoke shiftTailOverGap(elementData, a, b)con una bche può essere inferiore a size.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.