Java Streams: Replacing Groupingby and Reducing by Tomap

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 use Collectors.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 LineItems 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



Leave a reply



Submit