Reputation: 86747
It is possible to neglect some elements during Collectors.groupingBy
or Collectors.partitioningBy
?
Of course I know I can place a .filter()
anywhere in a stream. But my problem is that I have to run a rather complex and expensive evaluation, to decide in which "group" my objects should be divided into.
Moreover, there are always many objects that I would want to neglect during collecting.
Example: imagine a List<Foo>
, that I want to split into 2 lists. That's easy, but how can I additionally neglect all objects that don't fit into my evaluated condition?
var map = foos.stream().collect(Collectors.groupingBy(
foo -> {
int bar = complexEvaluation(foo);
if (bar > 1000) return true;
if (bar < 0) return false;
//TODO how to neglect the rest between 0-1000
},
Collectors.mapping(foo -> foo.id, Collectors.toSet())
));
Upvotes: 4
Views: 196
Reputation: 28988
If I understand your intent correctly you want to filter out some values while collecting them. It's doable with Collectors.filtering()
.
Note, that filtering()
can eliminate all values for some buckets, but it'll not cause empty buckets to be removed. In the example below buckets for 1, 2, 3, 4, 5 will be empty.
public static void main(String[] args) {
var foos = List.of(-100, 1, 2, 3, 4, 5, 1001, 1002, 1003);
var map = foos.stream()
.collect(Collectors.groupingBy(
UnaryOperator.identity(),
Collectors.filtering(foo -> foo < 0 || foo > 1000,
Collectors.toSet())));
System.out.println(map);
}
map
{1=[], 2=[], 3=[], -100=[-100], 4=[], 5=[], 1001=[1001], 1002=[1002], 1003=[1003]}
UPDATE
I revised this problem and the solution provided below computes the values beforehand.
In this version, undesired entries are filtered out in the stream, which makes the code inside Collector easier to read. I hope that instead of foo
objects integer is used isn't a problem.
public static void main(String[] args) {
List<Integer> foos = List.of(-100, 1, 2, 3, 4, 5, 101, 102, 103);
Map<Integer, Integer> fooToValue = getFooToValueMap(foos);
Map<Boolean, Set<Integer>> map = getFoosMap(fooToValue);
System.out.println(map);
}
private static Map<Boolean, Set<Integer>> getFoosMap(Map<Integer, Integer> fooToValue) {
return fooToValue.entrySet().stream()
.filter(entry -> entry.getValue() < 0 || entry.getValue() > 1000)
.collect(Collectors.partitioningBy(
entry -> entry.getValue() > 1000,
Collectors.mapping(Map.Entry::getKey, Collectors.toSet())
));
}
private static Map<Integer, Integer> getFooToValueMap(List<Integer> list) {
return list.stream()
.collect(Collectors.toMap(UnaryOperator.identity(), foo -> complexEvaluation(foo)));
}
private static int complexEvaluation(int foo) {
return (int) Math.signum(foo) * foo * foo;
}
map
{false=[-100], true=[101, 102, 103]}
Upvotes: 1
Reputation: 298203
If you want to avoid temporary storage, you have to implement your own collector:
var map = foos.parallelStream().collect(
() -> Map.of(true, new HashSet<ID>(), false, new HashSet<ID>()),
(o, foo) -> {
int bar = complexEvaluation(foo);
ID id = foo.id;
if (bar > 1000) o.get(true).add(id);
else if (bar < 0) o.get(false).add(id);
},
(a, b) -> { a.get(true).addAll(b.get(true)); a.get(false).addAll(b.get(false)); }
);
This example has the same behavior as partitioningBy
, always creating entries for true
and false
.
ID
is a placeholder for the type of foo.id
which you didn’t include in the question.
Upvotes: 2
Reputation: 20579
Just use an enum
to define your 3 cases:
enum Categories {
HIGH, LOW, NEGATIVE
}
var map = foos.stream().collect(Collectors.groupingBy(
foo -> {
int bar = complexEvaluation(foo);
if (bar > 1000) return HIGH;
if (bar < 0) return NEGATIVE;
return LOW;
},
Collectors.mapping(foo -> foo.id, Collectors.toSet())
));
Then ignore or remove LOW
if you don’t need it. It also has the added benefits of giving more meaning to your categories instead of just naming them true
/false
, and making it easier to refactor if you need more categories in the future.
Only drawback is that it builds a useless LOW
set, but that’s only an issue if it is really big compared to the other sets and the complexEvaluation()
operation.
Upvotes: 2
Reputation: 6228
To reuse the result of complexEvaluation in both filter
and groupingBy
, you can call before filtering and store the result in a wrapper class.
foos.stream()
.map(foo -> {
int bar = complexEvaluation(foo);
if (bar > 1000) Pair.of(foo, true);
if (bar < 0) Pair.of(foo, false);
return Pair.of(foo, null);
)
.filter(fooPair -> fooPair.getRight() != null)
.collect(Collectors.groupingBy(
Pair::getRight(),
Collectors.mapping(fooPair -> fooPair.getLeft().id, Collectors.toSet()
);
However, this is only if you insist on using groupingBy
for some reason.
An alternative with foreach would be much easier to read:
Map<Boolean, Set<Foo>> groups = new HashMap<>();
foos.stream()
.forEach(foo -> {
int bar = complexEvaluation(foo);
if (bar > 1000) groups.computeIfAbsent(true, k->new HashSet<>()).add(foo);
if (bar < 0) groups.computeIfAbsent(false, k->new HashSet<>()).add(foo);
})
Upvotes: 1