Java stream that is distinct by more than one property

自古美人都是妖i 提交于 2019-12-24 03:14:10

问题


I have following objects in the stream:

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

I would like to filter a stream based on following criteria:

eg. Having entries in stream: foo1 and foo2:

foo1 and foo2 have same values for a and b, but they differ in c property.

I would like to get rid of entries that have c higher in such case.


回答1:


Semantically equivalent to Eugene’s answer, but a bit simpler:

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())));

You need to group by a key holding both properties and due to the absence of a standard Pair type, you may use a List with two elements or a Map.Entry, both work. But using List is simpler (in Java 9, you would use List.of(…, …) which is even simpler) and has a better hash code if the same values may occur in both properties.

When the dowstream operation is a pure reduction, like selecting the minimum of the C property, the toMap collector fits better as it doesn’t require dealing with Optional.




回答2:


So if I understood correctly from your comments, it should look like this:

 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);



回答3:


There must be a nicer way to do this, but here's one solution.

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. For all elements in the list
  2. go through all elements,
  3. and find those with matching A and B
  4. sort by C and get the lowest
  5. keep element from step 1, if it is the Foo with the lowest C
  6. collect the results to a new list



回答4:


Simple solution is

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

but it requires ugly overriding in Foo, that can broke some another part of code

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

public int hashCode() {
    return a.hashCode() + b.hashCode();
}



回答5:


There's a way to do it without streams. I know the question specifically asks for a stream-based solution, but I think this is a good way to achieve the same. I'm writing this answer mainly as a complement to other answers, maybe it's useful for future readers.

Here's the code:

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);

This iterates the list and uses Map.merge to reduce Foo instances that have the same a and b.

Note: you can also do as Holger in his answer and reduce by using BinaryOperator.minBy:

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



回答6:


You can use groupBy to group your Foo objects and treat them as a list:

    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


来源:https://stackoverflow.com/questions/45078255/java-stream-that-is-distinct-by-more-than-one-property

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!