Java Streams: Replacing groupingBy and reducing by toMap
This pattern became evident by experience with using both collectors. You’ll find several Q&As on Stackoverflow, where a problem could be solved with either collector, but one of them seems a better fit for the particular task.
This is a variation of the difference between Reduction and Mutable Reduction. In the first case, we use reduce
on the Stream, in the second we use collect
. It comes naturally, that the groupingBy
collector, which takes a second Collector
as argument, is the right tool when we want to apply a Mutable Reduction to the groups.
Not that obviously, the toMap
collector taking a merge function is the right tool when we want to perform a classical Reduction, as that merge function has the same shape and purpose as a Reduction function, even if it is not called as such.
In practice, we note that the collectors which perform a Reduction, return an Optional
, which is usually not desired when being used with groupingBy
, which is the reason why toMap
works more smoothly in these cases.
There are surely more patterns which become apparent while using these APIs, but collecting them in one answer is not the scope of Stackoverflow.
Java 8 Lambda - Grouping & Reducing Object
In the context, Collectors.reducing
would help you reduce two Transaction
objects into a final object of the same type. In your existing code what you could have done to map to Result
type was to use Collectors.mapping
and then trying to reduce it.
But reducing without an identity provides and Optional
wrapped value for a possible absence. Hence your code would have looked like ;
Map<Integer, Map<String, Optional<Result>>> res = transactions.stream()
.collect(Collectors.groupingBy(Transaction::getYear,
Collectors.groupingBy(Transaction::getType,
Collectors.mapping(t -> new Result("YEAR_TYPE", t.getValue()),
Collectors.reducing((a, b) ->
new Result(a.getGroup(), a.getAmount() + b.getAmount()))))));
to thanks to Holger, one can simplify this further
…and instead of
Collectors.mapping(func, Collectors.reducing(op))
you
can useCollectors.reducing(id, func, op)
Instead of using this and a combination of Collectors.grouping
and Collectors.reducing
, transform the logic to use Collectors.toMap
as:
Map<Integer, Map<String, Result>> result = transactions.stream()
.collect(Collectors.groupingBy(Transaction::getYear,
Collectors.toMap(Transaction::getType,
t -> new Result("YEAR_TYPE", t.getValue()),
(a, b) -> new Result(a.getGroup(), a.getAmount() + b.getAmount()))));
The answer would stand complete with a follow-up read over Java Streams: Replacing groupingBy and reducing by toMap.
Correct syntax for Collectors.reducing
groupingBy
takes a Collector
as its second argument, so you should not pass the lambda lineItem -> ...
, and instead pass the Collector.reducing(...)
directly.
Also, since you are reducing a bunch of LineItem
s to one BigDecimal
, you should use the three-parameter overload of reducing
, with a mapper
public static <T, U> Collector<T,?,U> reducing(
U identity,
Function<? super T,? extends U> mapper,
BinaryOperator<U> op)
The mapper
is where you specify how a LineItem
into a BigDecimal
. You probably confused this with the second parameter of groupingBy
.
So to summarise:
Map<String,BigDecimal> totalByItem =
orders.stream()
.flatMap(order -> order.getLineItems().stream())
.collect(
Collectors.groupingBy(
LineItem::getName,
Collectors.reducing(
BigDecimal.ZERO,
LineItem::getPrice, // <----
BigDecimal::add
)
)
);
As Holger commented, the entire groupingBy
collector can also be replaced with a toMap
collector, without using reducing
at all.
.collect(
Collectors.toMap(
LineItem::getName, // key of the map
LineItem::getPrice, // value of the map
BigDecimal::add // what to do with the values when the keys duplicate
)
);
How can I combine the results from a Collectors.groupingBy
First, create your own MethodPair
class:
class MethodPair {
private final Method failure;
private final Method normal;
public MethodPair(Method failure, Method normal) {
this.failure = failure;
this.normal = normal;
}
public Method getFailure() {
return failure;
}
public Method getNormal() {
return normal;
}
public MethodPair combinedWith(MethodPair other) {
return new MethodPair(
this.failure == null ? other.failure : this.failure,
this.normal == null ? other.normal : this.normal)
);
}
}
Notice the combinedWith
method. This is going to useful in the reduction that we are going to do.
Instead of toList
, use the reducing
collector:
Map<String, MethodPair> map = ml.stream().collect(groupingBy(m -> {
var ann = m.getDeclaredAnnotation(MyTag.class);
return ann.anId();
}, TreeMap::new,
Collectors.reducing(new MethodPair(null, null), method -> {
var type = method.getDeclaredAnnotation(MyTag.class).type();
if (type == Type.NORMAL) {
return new MethodPair(null, method);
} else {
return new MethodPair(method, null);
}
}, MethodPair::combinedWith)
));
If you are fine with doing this in two steps, I would suggest that you create the Map<String, List<Method>>
first, then map its values to a new map. IMO this is more readable:
Map<String, List<Method>> map = ml.stream().collect(groupingBy(m -> {
var ann = m.getDeclaredAnnotation(MyTag.class);
return ann.anId();
}, TreeMap::new, toList()));
var result = map.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), entry -> {
Method normal = null;
Method failure = null;
for (var m : entry.getValue()) {
var type = m.getDeclaredAnnotation(MyTag.class).type();
if (type == Type.NORMAL && normal == null) {
normal = m;
} else if (type == Type.FAILURE && failure == null) {
failure = m;
}
if (normal != null && failure != null) {
break;
}
}
return new MethodPair(failure, normal);
}));
Stream groupingBy: reducing to first element of list
Actually, you need to use Collectors.toMap
here instead of Collectors.groupingBy
:
Map<String, Valuta> map =
getValute().stream()
.collect(Collectors.toMap(Valuta::getCodice, Function.identity()));
groupingBy
is used to group elements of a Stream based on a grouping function. 2 Stream elements that will have the same result with the grouping function will be collected into a List
by default.
toMap
will collect the elements into a Map
where the key is the result of applying a given key mapper and the value is the result of applying a value mapper. Note that toMap
, by default, will throw an exception if a duplicate is encountered.
Java 8 stream groupingBy. How to set field if only one object of grouping value?
Currency is not getting set to 0 as reducing
will not be evaluated for single sigle result. If you want to set all currency to 0, map it 0 as below,
list.stream().map(ele->{ele.setCurrency(0);return ele;}).collect(
groupingBy(SomeClass::getDate,
collectingAndThen(reducing((a, b) -> {
a.setPlayers(a.getPlayers() + b.getPlayers());
return a;
}), Optional::get)))
.values();
As correctly pointed by @Holger, you may want to use toMap
,
list.stream()
.map(ele->{ele.setCurrency(0);return ele;})
.collect(toMap(SomeClass::getDate, Function.identity(), (a, b) -> {
a.setPlayers(a.getPlayers() + b.getPlayers());
return a;
})).values();
Hope it helps.
Java stream: How to map to a single item when using groupingBy instead of to list
Use Collectors.toMap
collector to have a map of listed orders by customer id. After that, you can filter only those orders which are older than 6 months.
See the implementation below:
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Collections;
import java.util.Objects;
import java.util.Comparator;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
public static List<Long> getCustomerIdsOfOrdersOlderThanSixMonths(final List<Order> orderList) {
return Optional.ofNullable(orderList)
.orElse(Collections.emptyList())
.stream()
.filter(o -> Objects.nonNull(o) && Objects.nonNull(o.getOrderDate()))
.collect(Collectors.toMap(
Order::getCustomerId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Order::getOrderDate))))
.values()
.stream()
.filter(o -> o.getOrderDate()
.plusMonths(6)
.isBefore(ChronoLocalDate.from(LocalDate.now())))
.map(Order::getCustomerId)
.collect(Collectors.toList());
}
}
List<Long> customerIds = getCustomerIdsOfOrdersOlderThanSixMonths(orderList);
// [2, 4]
Java 8 Streams Map Reduced Value after Group By
You can wrap your reducing collector in CollectingAndThen
collector which takes a downstream collector and a finisher function. CollectingAndThen is a special collector that allows us to perform another action on a result straight after collecting ends. Change your map type to Map<String, String>
and do :
Map<String, String> itemTypesAndCosts = items.stream().collect(Collectors.groupingBy(Item::getType,
Collectors.collectingAndThen(
Collectors.reducing(BigDecimal.ZERO, Item::getCost, BigDecimal::add),
total -> new DecimalFormat("'$'0.00").format(total))
));
//output: {Food=$9.00, Toy=$6.00}
Related Topics
Swing on Osx: How to Trap Command-Q
Should I Declare Jackson's Objectmapper as a Static Field
Does Okhttp Support Accepting Self-Signed Ssl Certs
How to Extract a Tar File in Java
Why Is T Bounded by Object in the Collections.Max() Signature
How to Fix Ambiguous Type on Method Reference (Tostring of an Integer)
How to Asynchronously Call a Method in Java
How to Refer Environment Variable in Pom.Xml
How Is Values() Implemented for Java 6 Enums
Converting JSON to Xls/CSV in Java
Spring Boot 2.0.X Disable Security for Certain Profile
Run a Command Over Ssh with Jsch
Parsing JSON in Java Without Knowing JSON Format
How to Reference Another Property in Java.Util.Properties
How to Get Information About the Local Variables Using Java Reflection