看了ArrayList源码后,发现batchRemove还是较为复杂的,所以在这里写篇文章记录一下,以免以后再看时又头疼,哈哈哈。
先以自己的理解,弄一个简单版的,由浅入深,干掉他,如果读者发现有什么不正确的或有什么问题请及时指出哦。
先使用两个list模拟数组,从list1中移除list2存在的元素。
@Test
public void testBatchRemove() { // 遍历list1,从list1中移除list2存在的元素
// 初始化数据
List<Integer> list1 = new ArrayList<>(10);
for (int i = 0; i < 5; i++) {
list1.add(i);
}
// 初始化数据
List<Integer> list2 = new ArrayList<>(10);
list2.add(2);
// 修改list数据只通过set方法,r和w作为数组的索引
int r = 0, w = 0; // r:读, w:写
/**
* 从下面例子中可以看出
* 在第一轮和第二轮循环中,r和w是同步增加的
* 读会不停的往后读(即 r),但是写只会写入不被删除的数据
* example
* list1 = [0, 1, 2, 3, 4]
* list2 = [2]
* 第一轮循环结束 r = 1, w = 1 list1 = [0, 1, 2, 3, 4]
* 第二轮循环结束 r = 2, w = 2 list1 = [0, 1, 2, 3, 4]
* 第三轮循环结束 r = 3, w = 2 list1 = [0, 1, 2, 3, 4] 注:第三轮将不会进入if,导致w的更新比r慢一步
* 第四轮循环结束 r = 4, w = 3 list1 = [0, 1, 3, 3, 4] 注:元素 [2] 已被删除
* 第五轮循环结束 r = 5, w = 4 list1 = [1, 3, 4, 5, 5] 注:后面的元素将会向前推进一步
*/
for(;r < list1.size();r++) {
// 如果不包含,说明该位置的元素不能修改,则修改读(r)和写(w)的索引
// 如果包含,说明该位置的元素需要修改,则只修改读(r)的索引
if (!list2.contains(list1.get(r))) { // 搞清楚这个if就行啦!打断点看会舒服很多
list1.set(w++, list1.get(r));
}
// (r + 1)为了模拟for循环结束一轮的场景,因为打印的时候r不会++,
System.out.println(list1 + " w: " + w + " r: " + (r + 1));
}
// 只需要截取掉从0~w的元素即可
// 那怎么知道修改了多少次呢? 在截取之前使用 list.size() - w 即可知道修改了多少次元素 5 - 4 = 1
List<Integer> integers = list1.subList(0, w);
System.out.println(integers);
}
那问题来了,刚刚是移除list2中存在的元素,那要是想保留list2中存在的元素,怎么办?那难道还要再写一个方法吗?当然不是啦,只需要变一下就行啦~
增加变量
complement 是否补充数组中的数据,false则删除,true则保留
来了来了,新版本
/**
* 遍历list1,从list1中移除或只保留list2存在的元素
*/
@Test
public void testComplement() {
// 是否补充数组中的数据,false则删除,true则保留
boolean complement = false;
// 初始化数据
List<Integer> list1 = new ArrayList<>(10);
for (int i = 0; i < 5; i++) {
list1.add(i);
}
// 初始化数据
List<Integer> list2 = new ArrayList<>(10);
list2.add(2);
// 修改list数据只通过set方法,r和w作为数组的索引
int r = 0, w = 0; // r:读, w:写
for(;r < list1.size();r++) {
if (list2.contains(list1.get(r)) == complement) { // 搞清楚这个if就行啦!打断点看会舒服很多
list1.set(w++, list1.get(r));
}
// (r + 1)为了模拟for循环结束一轮的场景,因为打印的时候r不会++,
System.out.println(list1 + " w: " + w + " r: " + (r + 1));
}
List<Integer> integers = list1.subList(0, w);
System.out.println(integers);
}
继续。恩,是的,问题他又接着来了,现在有两个数组
list1 = [1, 2, 3, 4, 5, 6, 7]
list2 = [5, 6, 7]
像这样的,list1要删除list2中存在的元素,那list1中开头的第1~4个元素都不需要修改。现在这小数据不需要修改,那在生产环境中谁知道呢,哈哈,所以开头的优化下撒。
最终版本来了,这是你****的全新版本
/**
* 遍历list1,从list1中移除或只保留list2存在的元素
* 优化开头的操作
* list1.set(w++, list1.get(r));
*/
@Test
public void testOptimize() {
// 是否补充数组中的数据,false则删除,true则保留
boolean complement = false;
// 初始化数据
List<Integer> list1 = new ArrayList<>(10);
for (int i = 0; i < 5; i++) {
list1.add(i);
}
// 初始化数据
List<Integer> list2 = new ArrayList<>(10);
list2.add(3);
// 修改list数据只通过set方法,r和w作为数组的索引
int r = 0, w = 0; // r:读, w:写
/**
* 该for循环用于省略数组开头的赋值操作 :
* list1.set(w++, list1.get(r));
*/
for (;r < list1.size(); r++) {
// 如果没有要删除的操作,则直接返回即可,这样就完全省略了赋值操作
// list1.set(w++, list1.get(r));
if (r == list1.size()) {
return;
}
// 当需要删除是,则跳出
if (list2.contains(list1.get(r)) != complement) {
break;
}
// r 和 w同步增加
w++;
}
// 经过上个for循环的优化,该for循环的第一个操作必定的删除操作
// 由于r当前位置已经确定要被删除,所以r = r + 1,读取下一个
for(r = r + 1;r < list1.size();r++) {
if (list2.contains(list1.get(r)) == complement) { // 搞清楚这个if就行啦!打断点看会舒服很多
list1.set(w++, list1.get(r));
}
// (r + 1)为了模拟for循环结束一轮的场景,因为打印的时候r不会++,
System.out.println(list1 + " w: " + w + " r: " + (r + 1));
}
List<Integer> integers = list1.subList(0, w);
System.out.println(integers);
}
其实到这也差不多了,现原形!以下是JDK13的源码。
boolean batchRemove(Collection<?> c, boolean complement,
final int from, final int end) {
// 校验集合是否为空
Objects.requireNonNull(c);
final Object[] es = elementData;
// 优化
int r;
// Optimize for initial run of survivors
for (r = from;; r++) {
if (r == end)
return false;
if (c.contains(es[r]) != complement)
break;
}
// 和优化版本的代码同理 r = r + 1
int w = r++;
try {
for (Object e; r < end; r++)
if (c.contains(e = es[r]) == complement)
es[w++] = e;
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw ex;
} finally {
// 计算修改次数,应该是用来触发快速失败的
modCount += end - w;
// 收尾收尾
shiftTailOverGap(es, w, end);
}
return true;
}
之前看了JDK8的代码,发现有点不一样,无非是少了优化阶段,然后收尾阶段也没有封装,就这样撒。大家有什么问题的欢迎下面留言哦~