概述
对于List接口及其实现是日常开发中最常用的容器之一,对于List集合的并集、差集等操作也同样是频率极高。但是往往越是简单、常用的内容越容易让人放松警惕。
一步步把“栗子”丢进坑里
我们先说个简单的,我们已知有两个集合A和B,A={0,1,2,3,4,5}, B{2,3,4},现要求得到 A-B ,即 预期为 A = {0,1,5}
太简单了是不是,
List<Integer> list1 = new ArrayList<>();
list1.add(0);
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
List<Integer> list2 = new ArrayList<>();
list2.add(2);
list2.add(3);
list2.add(4);
list1.removeAll(list2);
System.out.println(list1);
如果我们对上面的需求稍加改动,我现在告诉大家,集合A={1,1,1,1,1,1},集合B={1,1,1},再求A-B,预期结果为 A = A-B = {1,1,1},你想如何实现呢?
相信有的朋友此时就掉进了坑里,不是和上面一样removeAll()一下就好了吗?!强大如ChatGPT 也犯了同样的错误。
ps:有的同学可能会问为啥你的ChatGPT可以在微信上? 文章末尾会有体验的入口哦!不要急,我们先把今天的主题聊透。如果针对上面的要求依然采用removeAll( )方法,你得到的只会是一个空集。
正确的写法应该是将B集合(list2)遍历,在循环中每次让A集合(list1)调用remove()方法:
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(1);
list1.add(1);
list1.add(1);
list1.add(1);
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(1);
list2.forEach(e->list1.remove(e));
System.out.println(list1);
不止于信
下面我来从源码角度详解一下remove()和removeAll()的本质区别,事实上removeAll()绝不仅仅是我们想当然的以为的:遍历执行remove()这么简单。 编程中最忌讳的就是把“我记得”、“我以为”挂在嘴边而写着原理的源码、接口注释、算法原理却看都没看过一眼。
我们现来看一下List顶级接口对这两个方法的解释:
remove(Object obj)
注意,remove方法还有一个实现是传int index,不是今天的重点所以不做展开。
/**
* Removes the first occurrence of the specified element from this list,
* if it is present (optional operation). If this list does not contain
* the element, it is unchanged. More formally, removes the element with
* the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list changed
* as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
* @throws ClassCastException if the type of the specified element
* is incompatible with this list
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if the specified element is null and this
* list does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws UnsupportedOperationException if the <tt>remove</tt> operation
* is not supported by this list
*/
boolean remove(Object o);
“Removes the first occurrence of the specified element from this list”,关键在first,移除的是集合中第一个相同的元素,我们再来看下ArrayList对于它的实现:
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);// 删除数据方法
return true;//删掉一个立刻返回,结束方法
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);// 删除数据方法
return true;//删掉一个立刻返回,结束方法
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
remove()方法是删掉了一个元素之后立刻返回的,这点很重要。
removeAll(Collection <?> c)
removeAll实际处理的是两个集合之间的关系
/**
* Removes from this list all of its elements that are contained in the
* specified collection (optional operation).
*
* @param c collection containing elements to be removed from this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws UnsupportedOperationException if the <tt>removeAll</tt> operation
* is not supported by this list
* @throws ClassCastException if the class of an element of this list
* is incompatible with the specified collection
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if this list contains a null element and the
* specified collection does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see #remove(Object)
* @see #contains(Object)
*/
boolean removeAll(Collection<?> c);
“Removes from this list all of its elements that are contained in the specified collection”,它旨在删除掉A集合中所有在B集合中出现过的元素。我们来看一下ArrayList对它的实现:
/**
* Removes from this list all of its elements that are contained in the
* specified collection.
*
* @param c collection containing elements to be removed from this list
* @return {@code true} if this list changed as a result of the call
* @throws ClassCastException if the class of an element of this list
* is incompatible with the specified collection
* (<a href="Collection.html#optional-restrictions">optional</a>)
* @throws NullPointerException if this list contains a null element and the
* specified collection does not permit null elements
* (<a href="Collection.html#optional-restrictions">optional</a>),
* or if the specified collection is null
* @see Collection#contains(Object)
*/
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++)
//complement = false,当elementData中的元素不在c中时,用这个元素替换前面与其相同的元素
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;
// 移除成功后,w为此时的size
size = w;
modified = true;
}
}
return modified;
}
这里给大家画图解释下removeAll的原理
它的核心思想是将重复的元素挪到集合后方,然后删除这部分重复的得到差集。这也就解释了为什么六个1和三个1 如果用 removeAll方法取差集会得到一个空集了。