最近都在匡扶汉室,无心写代码。
说到去重,往往会想到filter、distinct之类的关键字。
如果可以使用库的话,比如RxJava2中:
Observable.just("aa", "bb", "ccc", "dd", "bb").distinct().subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
System.out.println(s);
}
});
直接使用distinct操作符就可以实现,输出为:
aa
bb
ccc
dd
但实际应用中肯定不会因为一个去重功能或少数其他功能去使用这么大一个第三方库的,尤其是移动端。
所以一般是使用语言本身就有的特性。
Java中可以利用retainAll、removeAll、addAll等API组合起来实现去重。
先直接贴测试代码:
public class RemoveEquals {
public static void main(String[] args) {
List<Node> list1 = new ArrayList<>();
list1.add(new Node(1, "a1"));
list1.add(new Node(2, "a2"));
list1.add(new Node(3, "a3"));
list1.add(new Node(4, "a4"));
list1.add(new Node(5, "a5"));
List<Node> list2 = new ArrayList<>();
list2.add(new Node(1, "b1"));
list2.add(new Node(2, "b2"));
list2.add(new Node(6, "b6"));
list2.add(new Node(7, "b7"));
list2.add(new Node(8, "b8"));
list1.retainAll(list2);
System.out.println(list1);
list2.removeAll(list1);
System.out.println(list2);
list1.addAll(list2);
System.out.println(list1);
}
static class Node {
public int id;
public String attrs;
public Node(int id, String attrs) {
this.id = id;
this.attrs = attrs;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", attrs='" + attrs + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
return id == node.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
}
为了体现在各种比较中equals方法所起的作用,所以这里是创建了一个对象来作为元素,这个Node对象以id作为判断是否相等的依据。id相同,则对象相等。
最终输出为:
[Node{id=1, attrs='a1'}, Node{id=2, attrs='a2'}]
[Node{id=6, attrs='b6'}, Node{id=7, attrs='b7'}, Node{id=8, attrs='b8'}]
[Node{id=1, attrs='a1'}, Node{id=2, attrs='a2'}, Node{id=6, attrs='b6'}, Node{id=7, attrs='b7'}, Node{id=8, attrs='b8'}]
那么不用说,三个API的作用也大致清楚了。
allAll不必说是添加另一list的元素,retainAll是取交集,removeAll是取差集。
图中的位置对应关系就和API调用是一样的,如果是ListA调用的API,那么被取交集就是ListA,也就是说ListA中只剩下交集,灰色部分是已经在取交集过程中被删除掉了的,红色部分就是剩下的元素。
同样的,取差集也是这样的逻辑,ListA调用的API,被取的也是ListA,剩下的内容也是红色部分,灰色的是在取差集过程中被删除掉了。
emmmm…感觉画个图有点小题大作,不过问题不大。看着也许会直观一些。
当然,Java8其实也支持distinct操作符~
Java8引入了stream的概念,支持流式操作,其中就包括了filter、distinct、map等一系列常用操作符。
比如去重就可以这样写:
List<String> someStr = Arrays.asList("a", "b", "c", "e", "a","b");
List<String> toppledList = someStr.stream().distinct().collect(Collectors.toList());
System.out.println(toppledList);
输出即为:
[a, b, c, e]
可操作性还是很强的。
最后看下retainAll方法的源码,这个方法本来是List的接口方法之一,在ArrayList中是有重写的。当然Collection接口中也有这个retainAll方法,与这里不同:
//ArrayList.java
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
//当前List所有的元素
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//遍历当前List的所有项把所有相等项依次放到elementData中
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
//为了保证上面的代码抛出异常后,在异常之前取的交集也能保留,毕竟正常情况下在遍历完毕后r是与size相等的
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//将非交集项置为null,并将其元素项数量缩小
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;
}
可以看到这个ArrayList中的retainAll方法逻辑是遍历ListA的所有项,然后利用ListB.contains来判断是否有相等项,把相等项重新赋给当前索引的值。因为r绝对是大于等于w,所以不用担心值会丢失的问题。
至于ArrayList的removeAll方法,也是和retainAll用的同一个子方法,不过传入的参数不同:
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
传入的标记为false,也就是从“把相等项挑出来”变成“把不等项挑出来”。
至于Collection接口的retainAll和removeAll方法,思想也是一样的:
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
一样是通过遍历挑出等项或不等项,不过处理就变成“当即删除。因为是在Iterator中删除的缘故,所以不存在会有CurrentModificationException问题。也正是因为ArrayList中是数组结构,不好使用Iterator,所以才自行覆写了这两个方法处理。