前不久在微信公众号中看到一篇技术文章,内容是讨论“java中如何删除一个集合的多个元素”。
文章并没有把这个问题挖深,它的结论是,如果直接使用ArrayList,然后用forEach两层循环遍历,会报ConcurrentModificationException,尽管测试代码时单线程。原因是java的forEach使用iterator实现的循环遍历,而iterator在遍历的过程中是不允许删除元素的。也不能用传统for循环,因为使用for循环删除元素的过程中,数组下标有变动。
以上是文章的结论,现在呢,到底“删除集合的的多个元素”哪家强?
结论:
- 最简单的就是removeAll()
- 可以使用CopyOnWriteArrayList
我的测试代码:
public class RemoveElementsInList {
public void remove1(List<Integer> all, List<Integer> sub) {
for (int i = 0; i < all.size(); i ++) {
for (int j = 0; j < sub.size(); j ++) {
if (all.get(i).equals(sub.get(j))) {
all.remove(i);
}
}
}
System.out.println(all);
}
public static void main(String[] args) {
List<Integer> sub = new ArrayList<>(Arrays.asList(1,2,3));
List<Integer> all = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7));
all.removeAll(sub);
List<Integer> sub1 = new CopyOnWriteArrayList<>(sub);
List<Integer> all1 = new CopyOnWriteArrayList<>(all);
RemoveElementsInList removeElementsInList = new RemoveElementsInList();
removeElementsInList.remove1(all1, sub1);
}
}
下面看一看ArrayList的removeAll源码
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
这样的jdk源码,每个实现都是一道面试题。先抛出几个问题:
- 为什么要创建一个elementData引用,而不是直接在this.elementData上操作?
- removeAll的时间复杂度是多少?
- “// clear to let GC do its work"都做了些啥?
回答:
- 不知道。。。
- O(n^2)
- defragmentation。 手动设置引用为null,好让gc工作,使得内存中的碎片合并,腾出足以容纳大对象的空间。
我不确定1的答案是否跟3有关,反推一下,貌似也就是为了gc了吧。有待研究。
接下来,我们看看CopyOnWriteArrayList的removeAll方法的实现
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// temp array holds those elements we know we want to keep
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (!c.contains(element))
temp[newlen++] = element;
}
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
忽略锁的部分不看,逻辑还是很简单的,就是将结果保存到新创建的数组中。
CopyOnWriteArrayList的remove方法与removeAll类似,都是创建一个临时数组,将修改的内容复制到这个数组中,最后将这个数组的引用赋值给this引用。