对重复条目进行流过滤/减少



我正在尝试过滤/减少包含一些重复条目的数据流。

从本质上讲,我试图找到比我实现的更好的解决方案来过滤一组数据。我们的数据,在其基础上,是这样的:

Action | Date         | Detail
15     | 2016-03-15   | 
5      | 2016-03-15   | D1
5      | 2016-09-25   | D2      <--
5      | 2016-09-25   | D3      <-- same day, different detail
4      | 2017-02-08   | D4
4      | 2017-02-08   | D5
5      | 2017-03-01   | D6      <--
5      | 2017-03-05   | D6      <-- different day, same detail; need earliest
5      | 2017-03-08   | D7
5      | 2017-03-10   | D8
...

我需要提取详细信息,以便:

  • 仅选择操作 5
  • 如果细节相同(例如,D6在不同日期出现两次),则选择最早的日期

这些数据被加载到对象中(每个"记录"一个实例),对象上还有其他字段,但它们与此过滤无关。详细信息存储为字符串,日期存储为分区日期时间,操作是int(嗯,实际上是一个enum,但此处显示为int)。对象按时间顺序按List<Entry>给出。

我能够通过做以下工作来获得一个工作但我认为次优的解决方案:

List<Entry> entries = getEntries(); // retrieved from a server
final Set<String> update = new HashSet<>();
List<Entry> updates =
entries.stream()
.filter(e -> e.getType() == 5)
.filter(e -> pass(e, update))
.collect(Collectors.toList());

private boolean pass(Entry ehe, Set<String> update)
{
final String val =  ehe.getDetail();
if (update.contains(val)) { return false; }
update.add(val);
return true;
}

但问题是我必须使用这种pass()方法,并在其中检查Set<String>以维护是否处理了给定的详细信息。虽然这种方法有效,但似乎应该可以避免外部引用。

我尝试在详细信息上使用groupingBy,它将允许从列表中提取最早的条目,问题是我不再有日期排序,我必须处理结果Map<String,List<Entry>>

似乎在不使用pass()方法的情况下,这里应该可以进行一些reduce操作(如果我正确使用该术语),但是我正在努力获得更好的实现。

有什么更好的方法可以删除.filter(e -> pass(e, update))

谢谢!

这个答案中的两个解决方案,其中第二个要快得多。

解决方案 1

改编自Ole V.V.对另一个问题的回答:

Collection<Entry> result = 
entries.stream().filter(e -> e.getAction() == 5)
.collect(Collectors.groupingBy(Entry::getDetail, Collectors.collectingAndThen(Collectors.minBy(Comparator.comparing(Entry::getDate)), Optional::get)))
.values();

对于您最终得到的示例数据集(我选择 GMT+0 作为时区):

Entry [action=5, date=2017-03-01T00:00Z[GMT], detail=D6]
Entry [action=5, date=2017-03-08T00:00Z[GMT], detail=D7]
Entry [action=5, date=2017-03-10T00:00Z[GMT], detail=D8]
Entry [action=5, date=2016-03-15T00:00Z[GMT], detail=D1]
Entry [action=5, date=2016-09-25T00:00Z[GMT], detail=D2]
Entry [action=5, date=2016-09-25T00:00Z[GMT], detail=D3]

如果您坚持要找回List

List<Entry> result = new ArrayList<>(entries.stream() ..... .values());

如果您想取回原始订单,请使用 3 参数groupingBy

...groupingBy(Entry::getDetail, LinkedHashMap::new, Collectors.collectingAndThen(...))

解决方案 2

使用toMap,它更易于阅读且速度更快(请参阅 holi-java 对此答案的评论,以及下一个"部分"):

List<Entry> col = new ArrayList<>(
entries.stream().filter(e -> e.getAction() == 5)
.collect(Collectors.toMap(Entry::getDetail, Function.identity(), (a,b) -> a.getDate().compareTo(b.getDate()) >= 0 ? b : a))
.values());

其中(a,b) -> a.getDate().compareTo(b.getDate()) >= 0 ? b : a可以替换为:

BinaryOperator.minBy(Comparator.comparing(Entry::getDate))

如果您想在此解决方案中恢复原始订单,请使用 4 参数toMap

...toMap(Entry::getDetail, Function.identity(), (a,b) -> a.getDate().compareTo(b.getDate()) >= 0 ? b : a, LinkedHashMap::new)

性能

使用我为测试解决方案而创建的 testdata,我检查了两个解决方案的运行时。第一个解决方案平均需要 67 毫秒(只运行了 20 次,所以不要相信这些数字!),第二个解决方案平均需要 2 毫秒。如果有人想进行适当的性能比较,请将结果放在评论中,我将在此处添加。

如果我理解正确...

List<Entry> result = list.stream().collect(Collectors.toMap(
Entry::getDetail,
Function.identity(),
(left, right) -> {
return left.getDate().compareTo(right.getDate()) > 0 ? right : left;
}, LinkedHashMap::new))
.values()
.stream()
.filter(e -> e.getAction() == 5)
.collect(Collectors.toList());

您可以创建一个包含groupingByLinkedHashMap,该将保留广告顺序,这与HashMap不同。你是说列表已经按时间顺序排列,因此保留顺序就足够了。然后,可以简单地聚合此地图值中的列表。例如(添加静态导入):

List<Entry> selected = objs.stream()
.filter(e -> e.getType() == 5)
.collect(groupingBy(Entry::getDetail, LinkedHashMap::new, reducing((a, b) -> a)))
.values().stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toList());

reducing部分将保留 1 次或多次出现中的第一个。这是LinkedHashMap的文档和我正在使用的特定分组。

流接口为此提供了distinct方法。它将根据equals()对重复项进行排序。

因此,一种选择是,相应地实现Entryequals*方法,另一种选择是定义一个包装器类,该类根据特定条件(即getDetail())

class Wrapper {
final Entity entity;
Wrapper(Entity entity){
this.entity = entity;
}
Entity getEntity(){
return this.entity;
}
public boolean equals(Object o){
if(o instanceof Entity) {
return entity.getDetail().equals(((Wrapper) o).getEntity().getDetail());
}
return false;
}
public int hashCode() {
return entity != null ? entity.getDetail().hashCode() : 0;
}
}

除了包装、区分和取消映射您的实体之外:

entries.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::getEntity)
.collect(Collectors.toList());

如果流已排序,则始终使用第一个匹配项。列表流始终是有序的。

*)我首先尝试了它,但没有实现hashCode(),但失败了。原因是,java.util.stream.DistinctOps的内部使用HashSet来跟踪已经处理的元素,并检查contains,这依赖于hashCodeequals方法。因此,仅仅实施equals是不够的。

最新更新