【问题标题】:Stream filter/reduction on duplicated entries流过滤/减少重复条目
【发布时间】:2017-05-04 11:13:40
【问题描述】:

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

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

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 在不同的日子出现两次),则选择最早的日期

这些数据被加载到对象中(每个“记录”一个实例),对象上还有其他字段,但它们与此过滤无关。 Detail 存储为 String,Date 存储为 ZonedDateTime,Action 是 int(实际上是 enum,但这里显示为 int)。这些对象按时间顺序以List&lt;Entry&gt; 给出。

我能够通过以下方式获得一个有效但我认为不是最佳的解决方案:

  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&lt;String&gt; 来维护是否已经处理了给定的详细信息。虽然这种方法有效,但似乎应该可以避免外部引用。

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

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

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

谢谢!

【问题讨论】:

  • 它几乎是Java Stream: get latest version of user records的副本。看看你是否可以使用my answer there解决它。或其他答案之一。
  • @OleV.V.,我去看看。我在搜索时没有看到此问答。
  • 虽然确实不鼓励在 Streams 中使用这种pass 方法(并且您有更好的解决方案的答案),但处理Sets 的一般说明:Set.add 已经定义作为“如果不存在则添加”,它将返回值是否已添加,因此,您可以使用return update.add(val);代替if (update.contains(val)) { return false; } update.add(val); return true;进行两次哈希查找,更短更高效。
  • @Holger,关于Setupdate.add() 的要点很好。显然,我专注于流问题而没有清楚地考虑Set。我感谢详细的批评!

标签: java java-8 java-stream


【解决方案1】:

此答案中有两种解决方案,其中第二种解决方案明显更快。

解决方案 1

Ole V.V. 对另一个问题的the answer 改编:

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) -&gt; a.getDate().compareTo(b.getDate()) &gt;= 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)

性能

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

【讨论】:

  • 太棒了!比我首先收集到Map 的答案要好得多,而且您不依赖订单(可能不是)。一加
  • 感谢您重复使用我对另一个问题的回答,太好了。如果您使用 java.time 类作为日期,我建议使用 LocalDate 而不是 ZonedDateTime 来完成此任务。
  • 这里没有讽刺,我是认真的。我喜欢我写的东西似乎对某人有帮助,这可能是我在这里的主要原因。我认为我的贡献和你的贡献结合成更高的价值。
  • @Eugene 嗨,尤金。其实你的方法更好,只是你用错了顺序解决了问题。也许你忘记了BinaryOperatorminBy 方法。
  • @holi-java 由于您提出的解决方案已经在我的回答中,我已经移动了一些部分。第二种解决方案更快吗?它绝对更容易阅读。
【解决方案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());

【讨论】:

  • 我可能没有正确实施这个建议,但正如所写的那样,使用HashMap 似乎丢失了日期顺序。我可以通过使用LinkedHashMap(如 Manos 在另一个答案中建议的那样)或添加.sorted(Comparator.comparing(Entry::getDate) 来取回订单。我误解了实现吗?
  • @KevinO 你没有漏掉任何东西,我确实有类型,应该是LinkedHashMap
【解决方案3】:

您可以使用groupingBy 创建一个LinkedHashMap,这将保留与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 和我正在使用的特定groupingBy 的文档。

【讨论】:

  • 我看不到这解决了D6重复的问题
  • 谢谢@Eugene 我注意到自己并用reducing 而不是mapping 重写了我的答案中的代码。现在,如果 Java 8 能更好地处理 Optional 流就好了。
  • 是的。根据 OP:“对象按时间顺序在 List 中给出”
【解决方案4】:

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

因此,一种选择是相应地实现您的Entryequals* 方法,或者另一种选择是定义一个 Wrapper 类,它根据特定标准(即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 是不够的。

【讨论】:

  • 您确定以这种方式获得每个细节的最早条目吗?对我来说似乎并不明显。
  • 根据文档:For ordered streams, the selection of distinct elements is stable (for duplicated elements, the element appearing first in the encounter order is preserved.) For unordered streams, no stability guarantees are made
  • 谢谢。我相信代码可以工作。不过,我仍然不喜欢您的 equals() 声明对象相等,但实际上是不同的。
  • @GeraldMücke first 在遇到顺序 might not be 你真正想要的那个...
  • 提问者在他说的代码中按照遇到的顺序排在第一位,所以这可能已经足够好了。我也可能更喜欢最早约会。
猜你喜欢
  • 2020-02-16
  • 1970-01-01
  • 2018-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多