【问题标题】:Java stream that is distinct by more than one property由多个属性区分的 Java 流
【发布时间】:2017-12-18 02:18:28
【问题描述】:

我在流中有以下对象:

class Foo{
    String a;
    String b;
    int c;
}

我想根据以下条件过滤流:

例如。在流中有条目:foo1foo2

foo1foo2 具有相同的 ab 值,但它们的 c 属性不同。

在这种情况下,我想删除 c 更高的条目。

【问题讨论】:

  • 在这种情况下 1 和 2 是什么?
  • @Michael 更新答案
  • hm.. 更高呢?假设a="a", b="b", c = 1a="a", b="b", c = 2a="a", b="b", c = 3你想保留哪些?
  • @Eugene 在这种情况下我想保留第一个实体
  • @Eugene 是的,在列表中 a="a", b="b", c = 1a="a", b="b", c = 2a="a", b="b", c = 3a="a1", b="b", c = 1 我想在结果列表中有两个条目:第一个和最后一个

标签: java filtering java-stream distinct-values


【解决方案1】:

必须有更好的方法来做到这一点,但这里有一个解决方案。

List<Foo> list = new ArrayList<>();

list.stream().filter(foo ->
    list.stream()
    .filter(oth -> foo.a.equals(oth.a) && foo.b.equals(oth.b))
    .sorted(Comparator.comparingInt(x -> x.c))
    .findFirst()
    .equals(Optional.of(foo))
)
.collect(Collectors.toList());
  1. 对于列表中的所有元素
  2. 遍历所有元素,
  3. 并找到匹配 AB 的那些
  4. C排序,得到最低的
  5. 保留步骤 1 中的元素,如果它是具有最低 C 的 Foo
  6. 将结果收集到新列表中

【讨论】:

    【解决方案2】:

    您可以使用groupBy 对您的Foo 对象进行分组并将它们视为一个列表:

        List<Foo> filtered = list.stream()
                .collect(Collectors.groupingBy(
                    foo -> foo.a.hashCode() + foo.b.hashCode()))   // group them by attributes
                .values().stream()                                 // get a stream of List<Foo>
                .map(fooList -> {
                    fooList.sort((o1, o2) -> o2.c - o1.c);         // order the list
                    return fooList;
                })
                   .map(fooList -> {                               // if there is more than 1 item remove it
                       if (fooList.size() > 1)
                           return fooList.subList(0, fooList.size() - 1);
                       else
                           return fooList;
                   })
                .flatMap(Collection::stream)                        // Stream<List<Foo>> -> Stream<Foo>
                .collect(Collectors.toList());                      // collect
    

    【讨论】:

    • 如果fooList 有两个以上的元素怎么办?您实际上只想要第一个元素,而不是切断最后一个元素,那么为什么不使用get(0),而不是提取子列表后跟.flatMap(Collection::stream)?或者首先使用groupingBy(Function,Collector),以便在分组时获得最小c?但最糟糕的是按foo.a.hashCode() + foo.b.hashCode() 分组......即使ab 碰巧没有哈希冲突,建立两者的总和会大大增加获得一个的机会......
    • 目前还不清楚 OP 是否只想获取第一个元素或只是删除具有较高值的​​元素。如果我们只想得到最低的项目,答案可以简化很多!
    • 请注意问题的“摆脱 c 更高的条目”中的复数形式。并且删除“具有更高价值的那个”没有多大意义,因为它会是“具有最高价值的那个”......
    • 拥有相同ab的所有人中价值最高的人
    【解决方案3】:

    所以如果我从你的 cmets 中理解正确的话,它应该是这样的:

     List<Foo> foos = Stream.of(new Foo("a", "b", 1), new Foo("a", "b", 2), new Foo("a", "b", 3),
                new Foo("a", "bb", 3), new Foo("aa", "b", 3))
                .collect(Collectors.collectingAndThen(
                        Collectors.groupingBy(
                                x -> new AbstractMap.SimpleEntry<>(x.getA(), x.getB()),
                                Collectors.minBy(Comparator.comparing(Foo::getC))),
                        map -> map.values().stream().map(Optional::get).collect(Collectors.toList())));
    
        System.out.println(foos);
    

    【讨论】:

    【解决方案4】:

    简单的解决方案是

    .stream()
    .sorted((f1,f2) -> Integer.compare(f1.c, f2.c))
    .distinct()
    

    但它需要在 Foo 中进行丑陋的覆盖,这可能会破坏代码的另一部分

    public boolean equals(Object other) {
        return a.equals(((Foo)other).a) && b.equals(((Foo)other).b);
    }
    
    public int hashCode() {
        return a.hashCode() + b.hashCode();
    }
    

    【讨论】:

    • 当这样的事情需要自定义 equals 时,通常要做的事情是创建一个新类 FooWrapper 并制作一个流。
    【解决方案5】:

    语义上等同于Eugene’s answer,但更简单一些:

    List<Foo> foos = Stream.of(new Foo("a", "b", 1), new Foo("a", "b", 2),
                     new Foo("a", "b", 3), new Foo("a", "bb", 3), new Foo("aa", "b", 3))
        .collect(Collectors.collectingAndThen(
            Collectors.toMap(x -> Arrays.asList(x.getA(), x.getB()), x -> x,
                             BinaryOperator.minBy(Comparator.comparing(Foo::getC))),
                map -> new ArrayList<>(map.values())));
    

    您需要按包含两个属性的键进行分组,并且由于缺少标准的 Pair 类型,您可以使用带有两个元素的 ListMap.Entry,两者都可以。但是使用 List 更简单(在 Java 9 中,您将使用更简单的 List.of(…, …))并且如果两个属性中可能出现相同的值,则具有更好的哈希码。

    当下游操作是纯归约时,例如选择C 属性的最小值,toMap 收集器更适合,因为它不需要处理Optional

    【讨论】:

    • @Federico Peralta Schaffner:如果有遇到顺序,它将保留第一个遇到的,尽管在 Stackoverflow 上有一些讨论,这是否是保证行为,因为文档没有明确说明。我没有看到 OP 说他想保留一个以上的案例。
    • @Federico Peralta Schaffner:只有I would like to keep first entityI would like to have two entries in resulting list: first and last,其中第一个和最后一个是a="a", b="b", c = 1a="a1", b="b", c = 1a 属性的值不同。
    【解决方案6】:

    有一种方法可以在没有流的情况下做到这一点。我知道这个问题特别要求基于流的解决方案,但我认为这是实现相同目标的好方法。我写这个答案主要是作为对其他答案的补充,也许它对未来的读者有用。

    代码如下:

    List<Foo> list = Arrays.asList(
        new Foo("a", "b", 1),
        new Foo("a", "b", 2),
        new Foo("a", "b", 3),
        new Foo("a1", "b", 1));
    
    Map<List<String>, Foo> map = new HashMap<>();
    list.forEach(foo -> map.merge(Arrays.asList(foo.getA(), foo.getB()), foo,
        (oldFoo, newFoo) -> newFoo.getC() < oldFoo.getC() ? newFoo : oldFoo));
    Collection<Foo> distinct = map.values();
    
    System.out.println(distinct);
    

    这会迭代列表并使用Map.merge 来减少具有相同abFoo 实例。

    注意:您也可以在他的回答中像 Holger 一样使用BinaryOperator.minBy 来减少:

    list.forEach(foo -> map.merge(Arrays.asList(foo.getA(), foo.getB()), foo,
        BinaryOperator.minBy(Comparator.comparingInt(Foo::getC))));
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多