问题
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());
- For all elements in the list
- go through all elements,
- and find those with matching
A
andB
- sort by
C
and get the lowest - keep element from step 1, if it is the Foo with the lowest
C
- 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