前言
本文延续这个系列前两篇博客:ArrayList源码学习(一):初始化,扩容以及增删改查 和 ArrayList源码学习(二):迭代器,subList,这是完结的一篇。前两篇把 ArrayList 的源码基本说的差不多了,这一篇说一下之前剩下的 removeIf 方法,这个方法其实不是很好看懂,最后还有一点个人对于看源码的一些体会。
removeIf 方法
整体流程
在 ArrayList源码学习(一):初始化,扩容以及增删改查 这篇博客里,其实提到了 ArrayList 的删除元素的方法。这个 removeIf 位于源码最后,当时也没有看到。看一下代码:
public boolean removeIf(Predicate<? super E> filter) {
return removeIf(filter, 0, size);
}
复制代码
可以看出,最终还是通过调用removeIf(filter, 0, size)
来进行 removeIf,再看一下这个三参数版的 removeIf。
boolean removeIf(Predicate<? super E> filter, int i, final int end) {
// 过滤方法不能为空
Objects.requireNonNull(filter);
// 设置 expectedModCount 来避免并发修改
int expectedModCount = modCount;
final Object[] es = elementData;
// 找到第一个满足条件的 i
for (; i < end && !filter.test(elementAt(es, i)); i++)
;
if (i < end) {
// 找到所有满足删除条件的元素的位置,用 deathRow 存储
final int beg = i;
final long[] deathRow = nBits(end - beg);
deathRow[0] = 1L; // set bit 0
for (i = beg + 1; i < end; i++)
if (filter.test(elementAt(es, i)))
setBit(deathRow, i - beg);
// 是否并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
// 根据上面得到的索引下标,进行删除工作
int w = beg;
for (i = beg; i < end; i++)
if (isClear(deathRow, i - beg))
es[w++] = es[i];
// 善后工作
shiftTailOverGap(es, w, end);
return true;
} else {
// 是否并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return false;
}
}
复制代码
先针对整体代码做一个大概流程分析:先找到第一个满足删除条件的下标 i;接着从这个 i 开始,找到所有满足删除条件的位置并存储;最后根据前面得到的位置,进行删除,在代码的注释里,我也标注出来了,接下来具体分析代码。
找到第一个满足删除条件的 i
for (; i < end && !filter.test(elementAt(es, i)); i++) ;
复制代码
代码很简单,遇到第一个 filter.test(elementAt(es, i))
为真,即停止循环,或者 i == end
,说明没什么可删的。
找到所有满足删除条件的位置
if (i < end) {
// 找到所有满足删除条件的元素的位置,用 deathRow 存储
final int beg = i;
final long[] deathRow = nBits(end - beg);
deathRow[0] = 1L; // set bit 0
for (i = beg + 1; i < end; i++)
if (filter.test(elementAt(es, i)))
setBit(deathRow, i - beg);
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
// 其它代码
}
复制代码
这个 i 就是上个步骤的 i,如果 i >= end
,说明没什么可删的,不用执行这段代码,否则,咱们往下走。这块主要是两个语句不太好看懂:final long[] deathRow = nBits(end - beg);
和setBit(deathRow, i - beg);
,咱们分别看一下:
nBits 方法
private static long[] nBits(int n) {
return new long[((n - 1) >> 6) + 1];
}
复制代码
调用这个方法的语句是final long[] deathRow = nBits(end - beg);
,这个end - beg
也就是从 beg 开始到 end 结束,最多可能有这么多元素要删除,所以初始化 deathRow 自然要以这个为参数;接着看这个 nBits 方法:return new long[((n - 1) >> 6) + 1];
这个就是位运算的简单操作,翻译下就是:传的这个 n 如果是 1到64,那么最后的数组长度就是1,依此类推。这个时候,其实还不太清楚为啥这样,接着往下看。
setBit 方法
private static void setBit(long[] bits, int i) {
bits[i >> 6] |= 1L << i;
}
复制代码
调用这个方法的语句是
for (i = beg + 1; i < end; i++)
if (filter.test(elementAt(es, i)))
setBit(deathRow, i - beg);
复制代码
也就是从 beg + 1 一直遍历到 end,如果 filter.test(elementAt(es, i))
为真,则进行 setBit。这里 setBit 传的参数为i - beg
,也就是满足删除条件的这个索引和第一个满足条件的索引隔了多少个元素。进入 setBit,只有一条语句:bits[i >> 6] |= 1L << i;
,先通过 bits[i >> 6]
找到这个元素应该位于 bits 数组的哪一个元素,1到63位于0号元素,64-127位于1号元素,依次类推。这和上面创建 deathRow 正好对应上了。注意到初始化 deathRow 时有一句deathRow[0] = 1L;
,下面举个栗子理解下:
比方说 beg 为3,索引3的元素是第一个要删的元素,遍历之后发现索引5也要删除,那么 5 - 3 = 2,传入 setBit 的参数为2。bits[i >> 6]
发现位于0号元素,接着进行 |
操作,原来是 1L,也就是 0000000000........00001
,1之前63个0,现在进行 |
之后,变为:0000000000........00101
,接下来又发现索引66的元素要删,那么操作之后变为了:1000000000........00101
。
看到这,应该就有点头绪了,一个 long 64位,就可以存64个 01 状态,用这种方法来存储哪些要删,哪些不要删。
接那个例子,如果索引70的也要删,那么传入 setBit 的是67,计算之后发现位于 deathRow 的1号元素,一系列操作后,1号元素变为了:0000000000........01000
(左移时会取模,左移67就是左移3)。
现在终于知道怎么存待删除位置了,用 long 类型数组,一个 long 64 位,就存64个位置的状态。
根据位置进行删除
int w = beg;
for (i = beg; i < end; i++)
if (isClear(deathRow, i - beg))
es[w++] = es[i];
// 善后工作
shiftTailOverGap(es, w, end);
复制代码
有了前面的基础,再看这个删除代码就很简单了,isClear(deathRow, i - beg)
为真,说明这个位置比较'clear',可以要,es[w++] = es[i];
来存储它。看一下怎么判断一个位置是否'clear'的:
private static boolean isClear(long[] bits, int i) {
return (bits[i >> 6] & (1L << i)) == 0;
}
复制代码
调用语句为:isClear(deathRow, i - beg)
,传入参数为i - beg
,和 setBits 一样。还是先找到对应几号元素,接着进行 bits[i >> 6] & (1L << i)
操作,回想下 setBits 的操作:bits[i >> 6] |= 1L << i;
,如果这个位置满足删除条件,那么bits[i >> 6]
元素对应的位上一定为1,进行bits[i >> 6] & (1L << i)
操作,结果肯定不为0,如果结果为0,说明第一遍遍历,这个位置的元素不用删除,于是es[w++] = es[i];
来把它存下来。
还是举上面那个例子,1000000000........00101
这是咱们记录要删除的位置,那么现在遍历到索引为6的位置,这个位置不是要删的位置,由于 beg 为3,传的参数为 6 - 3 = 3,最终的操作为1000000000........00101
&0000000000........01000
,结果肯定是0。
对比 batchRemove
可以对比下之前的 batchRemove 方法是怎么删的:
// 删除的核心代码
for (Object e; r < end; r++)
if (c.contains(e = es[r]) == complement)
es[w++] = e;
复制代码
它是删的时候直接判断是否满足条件,removeIf 是先遍历一遍得到要删除位置,再遍历第二遍判断是否是要删的位置。理论上讲,batchRemove 也可以用 removeIf 的这种思想;同理,removeIf 我觉得也可以像 batchRemove 一样直接判断直接删。
小结
其实就是用一个 long 数组来表示哪些元素要被删,由于每个位置只有删和不删两个状态,可以用位来存储,所以用位运算巧妙的节省了很多空间,一个 long 元素一共可以保存64个信息,构思非常精巧,我也是北邮计算机科班出身,这种操作以前从未见过,大为震撼。
看源码的一些体会
也看了一些源码了,感觉还是学到一些东西,比方说今天说的这个用位来节省存储空间的操作;还有之前JUC包下面很多用逻辑表达式简化代码的手法;还有读写锁用 state 的高16位和低16位让一个变量表示两种状态;还有ArrayList的迭代器,subList 的设计等等。感觉这些思想都挺牛的。
举个栗子,周三力扣的每日一题:
412. Fizz Buzz
给你一个整数 n
,找出从 1
到 n
各个整数的 Fizz Buzz 表示,并用字符串数组 answer
(下标从 1 开始)返回结果,其中:
answer[i] == "FizzBuzz"
如果i
同时是3
和5
的倍数。answer[i] == "Fizz"
如果i
是3
的倍数。answer[i] == "Buzz"
如果i
是5
的倍数。answer[i] == i
如果上述条件全不满足。
题特别简单,但是正常写很多if else
语句,我用源码里学到的逻辑表达式性质来简化了代码:
public List<String> fizzBuzz(int n) {
List<String> list = new ArrayList<>();
for (int i = 1;i <= n;++i) {
if((i % 15 == 0 && list.add("FizzBuzz")) || (i % 5 == 0 && list.add("Buzz"))
|| (i % 3 == 0 && list.add("Fizz")) || list.add(String.valueOf(i)));
}
return list;
}
复制代码
代码看起来有技术含量,感觉就很舒服。