2

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.

Stefan Zobel
  • 3,182
  • 7
  • 28
  • 38
pixel
  • 24,905
  • 36
  • 149
  • 251

6 Answers6

3

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);
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • That’s going into the right direction, but [it can be simplified](https://stackoverflow.com/a/45083516/2711488)… – Holger Jul 13 '17 at 14:38
3

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • @Federico Peralta Schaffner: it will keep the first encountered, if there is an encounter order, though there was some discussion on Stackoverflow, whether this is guaranteed behavior, as the documentation is not explicit about it. I don’t see where the OP said that he wants to keep more than one that case. – Holger Jul 13 '17 at 16:30
  • @Federico Peralta Schaffner: there are only [I would like to keep first entity](https://stackoverflow.com/questions/45078255/java-stream-that-is-distinct-by-more-than-one-property/45083516?noredirect=1#comment77132730_45078255) and [I would like to have two entries in resulting list: first and last](https://stackoverflow.com/questions/45078255/java-stream-that-is-distinct-by-more-than-one-property/45083516?noredirect=1#comment77133197_45078255) where first and last are `a="a", b="b", c = 1` and `a="a1", b="b", c = 1`, having different values for the `a` property. – Holger Jul 13 '17 at 16:41
2

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
Michael
  • 41,989
  • 11
  • 82
  • 128
1

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();
}
rustot
  • 331
  • 1
  • 11
  • 1
    The usual thing to do when a custom `equals` is required for something like this is to create a new class `FooWrapper` and make a stream of those. – Michael Jul 13 '17 at 13:56
1

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))));
fps
  • 33,623
  • 8
  • 55
  • 110
0

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
Alberto S.
  • 7,409
  • 6
  • 27
  • 46
  • What if `fooList` has more than two elements? Instead of cutting off the last element, you actually want the just the first one, so why not use `get(0)`, instead of extracting sub-lists followed by `.flatMap(Collection::stream)`? Or using `groupingBy(Function,Collector)` in the first place, to get the minimum c right when grouping? But the worst thing is grouping by `foo.a.hashCode() + foo.b.hashCode()`… even if `a` and `b` happen to have no hash collision, building the sum of the two is raising the chance to get one dramatically… – Holger Jul 13 '17 at 14:35
  • It wasn't too clear if the OP wanted to get just the first element or just drop the one with the higher value. It we just want to get the lowest item the answer can be simplified a lot! – Alberto S. Jul 13 '17 at 15:34
  • 1
    Note the plural in “get rid of entries that have c higher” of the question. And dropping “the one with the higher value” wouldn’t make much sense as it would be “the one with the *highest* value” then… – Holger Jul 13 '17 at 15:44
  • The one with the highest value of all of them who has the same `a` and `b` – Alberto S. Jul 13 '17 at 17:42