Tenho uma pequena pergunta detalhada sobre a implementação que não consigo entender ArrayList::removeIf
. Eu não acho que posso simplesmente colocar do jeito que está, sem algumas pré-condições primeiro.
Como tal: a implementação é basicamente em massa remove
, ao contrário ArrayList::remove
. Um exemplo deve facilitar muito a compreensão das coisas. Digamos que eu tenho esta lista:
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 eu gostaria de remover todos os elementos que são iguais. Eu poderia fazer:
Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
int elem = iter.next();
if (elem % 2 == 0) {
iter.remove();
}
}
Ou :
list.removeIf(x -> x % 2 == 0);
O resultado será o mesmo, mas a implementação é muito diferente. Como iterator
é uma visão do ArrayList
, toda vez que eu chamo remove
, o subjacente ArrayList
deve ser levado a um estado "bom", o que significa que a matriz interna realmente mudará. Novamente, em cada chamada de remove
, haverá chamadas System::arrayCopy
internamente.
Pelo contrário, removeIf
é mais inteligente. Como faz a iteração internamente, pode tornar as coisas mais otimizadas. A maneira como faz isso é interessante.
Primeiro calcula os índices de onde os elementos devem ser removidos. Isso é feito computando primeiro uma minúscula BitSet
matriz de long
valores em que, em cada índice, reside um 64 bit
valor (a long
). Vários 64 bit
valores tornam isso a BitSet
. Para definir um valor em um deslocamento específico, primeiro você precisa descobrir o índice na matriz e depois definir o bit correspondente. Isso não é muito complicado. Digamos que você queira definir os bits 65 e 3. Primeiro, precisamos de um long [] l = new long[2]
(porque fomos além de 64 bits, mas não mais que 128):
|0...(60 more bits here)...000|0...(60 more bits here)...000|
Você primeiro encontra o índice: 65 / 64
(eles realmente o fazem 65 >> 6
) e, em seguida, nesse índice ( 1
) coloca o bit necessário:
1L << 65 // this will "jump" the first 64 bits, so this will actually become 00000...10.
A mesma coisa para 3
. Como tal, essa matriz longa se tornará:
|0...(60 more bits here)...010|0...(60 more bits here)...1000|
No código fonte, eles chamam isso de BitSet - deathRow
(belo nome!).
Vamos pegar esse even
exemplo aqui, ondelist = 2, 4, 6, 5, 5
- eles iteram a matriz e calculam isso
deathRow
(ondePredicate::test
estátrue
).
deathRow = 7 (000 ... 111)
índices de significado = [0, 1, 2] devem ser removidos
- agora eles substituem elementos na matriz subjacente com base nesse deathRow (sem entrar em detalhes como isso é feito)
matriz interna torna-se: [5, 5, 6, 5, 5]. Basicamente, eles movem os elementos que deveriam permanecer na frente da matriz.
Eu posso finalmente trazer a pergunta.
Neste momento, eles sabem:
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
Para mim, há um único passo a ser feito aqui:
void getRidOfElementsFromWToEnd() {
for(int i=w; i<end; ++i){
es[i] = null;
}
size = w;
}
Em vez disso, isso acontece:
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;
}
Renomeei as variáveis de propósito aqui.
Qual é o sentido de chamar:
System.arraycopy(es, end, es, w, size - end);
Especialmente size - end
, já que end
é size
o tempo todo - nunca é alterado (portanto, é sempre zero
). Este é basicamente um NO-OP aqui. Que caso de esquina estou perdendo aqui?
System.arraycopy(es, end, es, w, size - end)
como detalhes de implementação subjacentesremoveIf
? Eu quase senti que estava lendo uma resposta para outra pergunta no meio. (Lendo o comentário acima) Eu sinto que acabou em uma pergunta trivial finalmente. É assim mesmo?System.arrayCopy
. Não obstante, foi uma jornada divertida pelos detalhes (esse conjunto de bits interno acaba tendo a mesma idéiajava.util.BitSet
)range
...) e eu a aceitarei.java.util.BitSet
. Para mim, a reimplementação dasBitSet
operações não parece significativamente melhor que a original. A oportunidade de pular palavras inteiras foi perdida.Respostas:
Você está analisando o caso específico (comum) em que a lista, você chama
removeIf
, é a mesma que aArrayList
. Somente neste caso, você pode assumir queend
é sempre igual asize
.Um contra-exemplo seria:
Da mesma forma,
removeAll
chamaráshiftTailOverGap
com umend
argumento que pode diferir desize
quando aplicado a umsubList
.Uma situação semelhante surge quando você liga
clear()
. Nesse caso, a operação real, executada ao chamá-la porArrayList
si mesma, é tão trivial que nem chama oshiftTailOverGap
método. Somente ao usar algo comol.subList(a, b).clear()
, ele terminaráremoveRange(a, b)
eml
, que por sua vez, como você já descobriu, invocaráshiftTailOverGap(elementData, a, b)
com umb
que pode ser menor quesize
.fonte