Working with nested maps using Stream API - java

My current approach exploiting Streams API in conjunction with forEach loop:
public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product,Integer>> shopping) {
Map<String, Client> result = new HashMap<>();
Map<Client, Map<String, BigDecimal>> temp =
shopping.entrySet()
.stream()
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.flatMapping(e -> e.getValue().entrySet().stream(),
Collectors.groupingBy(e -> e.getKey().getCategory(),
Collectors.mapping(ee -> ee.getKey().getPrice().multiply(BigDecimal.valueOf(ee.getValue())),
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))))));
/*curious, how could I refactor that piece of code, so the method uses only one stream chain? */
temp.forEach((client, value)
-> value.forEach((category, value1)
-> {
if (!result.containsKey(category) ||
temp.get(result.get(category)).get(category).compareTo(value1) < 0)
result.put(category, client);
}));
return result;
}
As the method's name sugggests, I want to find a map Map <String, Client>, containing Client with most purchases (as value) in specified category (as key) in each product's category
shopping is basically a map: Map<Client, Map<Product,Integer>>,
The outer Key represents the Client
The inner Key represents the Product. Product class members are name, category, price (BigDecimal)
the inner maps value (Integer) reprents the number of specified product which belong to a specific client
Not sure, if that's even possible? Collectors.collectingAndThen maybe could be useful?

You can use StreamEx library, and do smth like this
public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {
return EntryStream.of(shopping)
.flatMapKeyValue(((client, productQuantityMap) ->
EntryStream.of(productQuantityMap)
.mapToValue((p, q) -> p.getPrice().multiply(BigDecimal.valueOf(q)))
.mapKeys(Product::getCategory)
.map(e -> new ClientCategorySpend(client, e.getKey(), e.getValue())))
)
.groupingBy(
ClientCategorySpend::getCategory,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(ClientCategorySpend::getSpend)),
t -> t.get().getClient())
);
}

You were pretty much doomed the moment you grouped by client. The
top level Collectors.groupingBy must use the category as the grouping by key.
To do that you would flatMap before collecting, so you get a flat stream of
client + category + spend elements.
Here's one way to do it. I'll first define a POJO for the elements of the flattened stream:
static class ClientCategorySpend
{
private final Client client;
private final String category;
private final BigDecimal spend;
public ClientCategorySpend(Client client, String category, BigDecimal spend)
{
this.client = client;
this.category = category;
this.spend = spend;
}
public String getCategory()
{
return category;
}
public Client getClient()
{
return client;
}
public BigDecimal getSpend()
{
return spend;
}
}
And now the function:
public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping)
{
// <1>
Collector<? super ClientCategorySpend, ?, BigDecimal> sumOfSpendByClient = Collectors.mapping(ClientCategorySpend::getSpend,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add));
// <2>
Collector<? super ClientCategorySpend, ?, Map<Client, BigDecimal>> clientSpendByCategory = Collectors.groupingBy(
ClientCategorySpend::getClient,
sumOfSpendByClient
);
// <3>
Collector<? super ClientCategorySpend, ?, Client> maxSpendingClientByCategory = Collectors.collectingAndThen(
clientSpendByCategory,
map -> map.entrySet().stream()
.max(Comparator.comparing(Map.Entry::getValue))
.map(Map.Entry::getKey).get()
);
return shopping.entrySet().stream()
// <4>
.flatMap(
entry -> entry.getValue().entrySet().stream().map(
entry2 -> new ClientCategorySpend(entry.getKey(),
entry2.getKey().category,
entry2.getKey().price.multiply(BigDecimal.valueOf(entry2.getValue())))
)
).collect(Collectors.groupingBy(ClientCategorySpend::getCategory, maxSpendingClientByCategory));
}
Once I have a stream of ClientCategorySpend (4), I group it by category. I use
the clientSpendByCategory collector (2) to create a map between the client and the total spend in the category. This in turn depends on sumToSpendByClient (1) which is basically a reducer that sums up the spends. You then get to use collectingAndThen as you suggested,
reducing each Map<Client, BigDecimal> to a single client using max.

This should do ;)
public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {
return shopping
.entrySet()
.stream()
.map(entry -> Pair.of(
entry.getKey(),
entry.getValue()
.entrySet()
.stream()
.map(e -> Pair.of(
e.getKey().getCategory(),
e.getKey().getPrice().multiply(
BigDecimal.valueOf(e.getValue()))))
.collect(Collectors.toMap(
Pair::getKey,
Pair::getValue,
BigDecimal::add))))
// Here we have: Stream<Pair<Client, Map<String, BigDecimal>>>
// e.g.: per each Client we have a map { category -> purchase value }
.flatMap(item -> item.getValue()
.entrySet()
.stream()
.map(e -> Pair.of(
e.getKey(), Pair.of(item.getKey(), e.getValue()))))
// Here: Stream<Pair<String, Pair<Client, BigDecimal>>>
// e.g.: entries stream { category, { client, purchase value } }
// where there are category duplicates, so we must select those
// with highest purchase value for each category.
.collect(Collectors.toMap(
Pair::getKey,
Pair::getValue,
(o1, o2) -> o2.getValue().compareTo(o1.getValue()) > 0 ?
o2 : o1))
// Now we have: Map<String, Pair<Client, BigDecimal>>,
// e.g.: { category -> { client, purchase value } }
// so just get rid of unnecessary purchase value...
.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().getKey()));
}
Pair is org.apache.commons.lang3.tuple.Pair. If you do not want to use Appache Commons library you may use java.util.AbstractMap.SimpleEntry instead.

Related

How to use Map.computeIfAbsent() in a stream?

I have a fun puzzler. Say I have a list of String values:
["A", "B", "C"]
Then I have to query another system for a Map<User, Long> of users with an attribute that corresponds to those values in the list with a count:
{name="Annie", key="A"} -> 23
{name="Paul", key="C"} -> 16
I need to return a new List<UserCount> with a count of each key. So I expect:
{key="A", count=23},
{key="B", count=0},
{key="C", count=16}
But I'm having a hard time computing when one of my User objects has no corresponding count in the map.
I know that map.computeIfAbsent() does what I need, but how can I apply it based on what's on the contents of the original list?
I think I need to stream the over the original list, then apply compute? So I have:
valuesList.stream()
.map(it -> valuesMap.computeIfAbsent(it.getKey(), k-> OL))
...
But here's where I get stuck. Can anyone provide any insight as to how I accomplish what I need?
You can create an auxiliary Map<String, Long> which will associate each string key with the count and then generate a list of UserCount based on it.
Example:
public record User(String name, String key) {}
public record UserCount(String key, long count) {}
public static void main(String[] args) {
List<String> keys = List.of("A", "B", "C");
Map<User, Long> countByUser =
Map.of(new User("Annie", "A"), 23L,
new User("Paul", "C"), 16L));
Map<String, Long> countByKey = countByUser.entrySet().stream()
.collect(Collectors.groupingBy(entry -> entry.getKey().key(),
Collectors.summingLong(Map.Entry::getValue)));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
System.out.println(userCounts);
}
Output
[UserCount[key=A, count=23], UserCount[key=B, count=0], UserCount[key=C, count=16]]
Regarding the idea of utilizing computeIfAbsent() with stream - this approach is wrong and discouraged by the documentation of the Stream API.
Sure, you can use computeIfAbsent() to solve this problem, but not in conjunction with streams. It's not a good idea to create a stream that operates via side effects (at least without compelling reason).
And I guess you even don't need Java 8 computeIfAbsent(), plain and simple putIfAbsent() will be sufficient.
The following code will produce the same result:
Map<String, Long> countByKey = new HashMap<>();
countByUser.forEach((k, v) -> countByKey.merge(k.key(), v, Long::sum));
keys.forEach(k -> countByKey.putIfAbsent(k, 0L));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
And instead of applying forEach() on a map and list, you can create two enhanced for loops if this options looks convoluted.
Another educational and parallel friendly version would be to gather the logic in one place and build your own custom accumulator and combiner for the Collector
public static void main(String[] args) {
Map<User, Long> countByUser =
Map.of(new User("Alice", "A"), 23L,
new User("Bob", "C"), 16L);
List<String> keys = List.of("A", "B", "C");
UserCountAggregator userCountAggregator =
countByUser.entrySet()
.parallelStream()
.collect(UserCountAggregator::new,
UserCountAggregator::accumulator,
UserCountAggregator::combiner);
List<UserCount> userCounts = userCountAggregator.getUserCounts(keys);
System.out.println(userCounts);
}
Output
[UserCount(key=A, count=23), UserCount(key=B, count=0), UserCount(key=C, count=16)]
User and UserCount classes with Lombok's #Value
#Value
class User {
private String name;
private String key;
}
#Value
class UserCount {
private String key;
private long count;
}
And the UserCountAggregator which contains your custom accumulator and combiner
class UserCountAggregator {
private Map<String, Long> keyCounts = new HashMap<>();
public void accumulator(Map.Entry<User, Long> userLongEntry) {
keyCounts.put(userLongEntry.getKey().getKey(),
keyCounts.getOrDefault(userLongEntry.getKey().getKey(), 0L)
+ userLongEntry.getValue());
}
public void combiner(UserCountAggregator other) {
other.keyCounts
.forEach((key, value) -> keyCounts.merge(key, value, Long::sum));
}
public List<UserCount> getUserCounts(List<String> keys) {
return keys.stream()
.map(key -> new UserCount(key, keyCounts.getOrDefault(key, 0L)))
.collect(Collectors.toList());
}
}
final Map<User,Long> valuesMap = ...
// First, map keys to counts (assuming keys are unique for each user)
final Map<String,Long> keyToCountMap = valuesMap.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey().key, e -> e.getValue()));
final List<UserCount> list = valuesList.stream()
.map(key -> new UserCount (key, keyToCountMap.getOrDefault(key, 0L)))
.collect(Collectors.toList());

Java8 How to convert 3-level nested list into nested HashMap using stream and lambda

I'm trying to convert a 3 level nested list into Nested HashMap.
The function declaration for the same is:
Map<Key1, Map<Key2, List<String>>> transformToMap (List<Obj1> inputList)
The inputList internally has nested list which again has nested list.
The code I've wrote is using traditional for loop as follow:
private Map<Key1 , Map<Key2, List<String>>> reverseLookup(List<Key2> key2List){
Map<Key1 , Map<Key2, List<String>>> resultMap = new HashMap<>();
key2List.forEach(key2->{
List<ElementObject> elementObjects = key2.getElementObjects();
elementObjects.forEach(elementObject->{
final String name = elementObject.getName();
elementObject.getApplicablePeriods().forEach(applicablePeriod-> {
Key1 key1 = applicablePeriod.getKey1();
Map<Key2, List<String>> map2 = resultMap.get(key1);
if(map2 == null){
map2 = new HashMap<>();
}
List<String> stringList = map2.get(key2);
if(stringList == null){
stringList = new ArrayList<>();
}
stringList.add(name);
map2.put(key2, stringList);
resultMap.put(key1, map2);
});
});
});
return resultMap;
}
The class structure for the same is as follow:
class Key2{
List<ElementObject> elementObjects;
//getters & setters
}
class ElementObject {
String name;
//few more params
List<ApplicablePeriod> applicablePeriods;
//getters & setters
}
class ApplicablePeriod{
Key1 key1;
//getters & setters
}
class Key1{
//some parameters
//getters & setters
}
The above code is fulfilling my expectations.
What will be the efficient way to transform it into stream lambda using Collectors.toMap ?
I've tried something as follow:
inputList
.stream()
.flatMap(item -> item.getObj2List().stream())
.flatMap(nestedItem -> nestedItem.getKeyList().stream())
.collect(Collectors.toMap(a-> a.get()))
But not getting what should be the next step in Collectors.toMap.
Not able to handle final String name = nestedItem.getName(); which is used just before 3rd for loop.
Let me know the way to solve this.
I don't have any test data to see if it creates something similar as your traditional code. But take a look at this and let me know if it helps:
key2List.stream().flatMap((key2) -> key2.elementObjects.stream().map((element) -> new AbstractMap.SimpleImmutableEntry<>(key2, element)))
.flatMap((entry) -> entry.getValue().applicablePeriods.stream().map((period) -> new AbstractMap.SimpleImmutableEntry<>(period.key1, new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue().name))))
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())))));
In fact, the problem is that you want to access multiple levels of abstractions inside of the same Stream, this is usually not possible unless you have
Nested streams
An object that can hold references to higher objects
I fixed it in the second way.
I'm using java-16 and the following features
Collectors#mapMulti
Collectors#groupingBy
A locally defined record
private Map<Key1, Map<Key2, List<String>>> reverseLookup(List<Key2> key2List) {
record HolderObject(Key2 key2, ElementObject elementObject, ApplicablePeriod applicablePeriod){}
return key2List.stream()
.mapMulti((Key2 key2, Consumer<HolderObject> consumer) -> {
List<ElementObject> elementObjects = key2.getElementObjects();
elementObjects.forEach(elementObject ->
elementObject.getApplicablePeriods().forEach(applicablePeriod -> {
consumer.accept(new HolderObject(
key2,
elementObject,
applicablePeriod
));
})
);
})
.collect(Collectors.groupingBy(
h -> h.applicablePeriod().getKey1(),
Collectors.groupingBy(
HolderObject::key2,
Collectors.mapping(
h -> h.elementObject().getName(),
Collectors.toList()
)
)
));
}
And here is a java-8 compatible solution
private Map<Key1, Map<Key2, List<String>>> reverseLookup(List<Key2> key2List) {
return key2List
.stream()
.flatMap(key2 -> key2.getElementObjects()
.stream()
.flatMap(elementObject -> elementObject.getApplicablePeriods()
.stream()
.map(applicablePeriod -> new HolderObject(
key2,
elementObject,
applicablePeriod
))))
.collect(Collectors.groupingBy(
h -> h.getApplicablePeriod().getKey1(),
Collectors.groupingBy(
HolderObject::getKey2,
Collectors.mapping(
h -> h.getElementObject().getName(),
Collectors.toList()
)
)
));
}
#Value
public static class HolderObject {
Key2 key2;
ElementObject elementObject;
ApplicablePeriod applicablePeriod;
}

Adding two lists of own type

I have a simple User class with a String and an int property.
I would like to add two Lists of users this way:
if the String equals then the numbers should be added and that would be its new value.
The new list should include all users with proper values.
Like this:
List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}
User definition:
public class User {
private String name;
private int comments;
}
My method:
public List<User> addTwoList(List<User> first, List<User> sec) {
List<User> result = new ArrayList<>();
for (int i=0; i<first.size(); i++) {
Boolean bsin = false;
Boolean isin = false;
for (int j=0; j<sec.size(); j++) {
isin = false;
if (first.get(i).getName().equals(sec.get(j).getName())) {
int value= first.get(i).getComments() + sec.get(j).getComments();
result.add(new User(first.get(i).getName(), value));
isin = true;
bsin = true;
}
if (!isin) {result.add(sec.get(j));}
}
if (!bsin) {result.add(first.get(i));}
}
return result;
}
But it adds a whole lot of things to the list.
This is better done via the toMap collector:
Collection<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values();
First, concatenate both the lists into a single Stream<User> via Stream.concat.
Second, we use the toMap collector to merge users that happen to have the same Name and get back a result of Collection<User>.
if you strictly want a List<User> then pass the result into the ArrayList constructor i.e. List<User> resultSet = new ArrayList<>(result);
Kudos to #davidxxx, you could collect to a list directly from the pipeline and avoid an intermediate variable creation with:
List<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values()
.stream()
.collect(Collectors.toList());
You have to use an intermediate map to merge users from both lists by summing their ages.
One way is with streams, as shown in Aomine's answer. Here's another way, without streams:
Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Now, you can create a list of users, as follows:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));
This assumes User has a constructor that accepts name and comments.
EDIT: As suggested by #davidxxx, we could improve the code by factoring out the first part:
BiConsumer<List<User>, Map<String, Integer>> action = (list, map) ->
list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);
This refactor would avoid DRY.
There is a pretty direct way using Collectors.groupingBy and Collectors.reducing which doesnt require setters, which is the biggest advantage since you can keep the User immutable:
Collection<Optional<User>> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
// and reducing the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
.values(); // return values from Map<String, List<User>>
Unfortunately, the result is Collection<Optional<User>> since the reducing pipeline returns Optional since the result might not be present after all. You can stream the values and use the map() to get rid of the Optional or use Collectors.collectAndThen*:
Collection<User> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
Collectors.collectingAndThen( // reduce the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())),
Optional::get))) // and extract from the Optional
.values();
* Thanks to #Aomine
As alternative fairly straight and efficient :
stream the elements
collect them into a Map<String, Integer> to associate each name to the sum of comments (int)
stream the entries of the collected map to create the List of User.
Alternatively for the third step you could apply a finishing transformation to the Map collector with collectingAndThen(groupingBy()..., m -> ...
but I don't find it always very readable and here we could do without.
It would give :
List<User> users =
Stream.concat(first.stream(), second.stream())
.collect(groupingBy(User::getName, summingInt(User::getComments)))
.entrySet()
.stream()
.map(e -> new User(e.getKey(), e.getValue()))
.collect(toList());

Process list stream and collect into map/ImmutableMap with only non null values

How to process a list of string and collec it into Map or Immutable map only for those whose value is present
String anotherParam = "xyz";
Map.Builder<String,String> resultMap = ImmutableMap.builder(..)
listOfItems.stream()
.filter(Objects::nonNull)
.distinct()
.forEach(
item -> {
final Optional<String> result =
getProcessedItem(item,anotherParam);
if (result.isPresent()) {
resultMap.put(item, result.get());
}
});
return resultMap.build();
Please tell, is there a better way to achieve this via collect?
If you have access to Apache Commons library you can make use of Pair.class
Map<String, String> resultMap = ImmutableMap.copyof(listOfItems()
.stream()
.filter(Objects::nonNull)
.distinct()
.map(it -> Pair.of(it, getProcessedItem(it,anotherParam))
.filter(pair -> pair.getValue().isPresent())
.collect(toMap(Pair::getKey, pair -> pair.getValue().get())))
But it's a good practice to make special data classes which describes your mapping item->result more specificly
Here is an example, create class like this:
static class ItemResult(){
public final String item;
public final Optional<String> result;
public ItemResult(String item, Optional<String> result){
this.item = item;
this.result = result;
}
public boolean isPresent(){
return this.result.isPresent();
}
public String getResult(){
return result.get();
}
}
And use it like that:
Map<String, String> resultMap = ImmutableMap.copyOf(listOfItems()
.stream()
.filter(Objects::nonNull)
.distinct()
.map(it -> new ItemResult(it, getProcessedItem(it,anotherParam))
.filter(ItemResult::isPresent)
.collect(toMap(ItemResult::item, ItemResult::getResult)))
You can read here why Google gave up the idea of tuples and pairs and don't use them in most cases
If after all you don't want to use any other class you can leverage api of the Optional:
Map.Builder<String,String> resultMap = ImmutableMap.builder(..)
listOfItems.stream()
.filter(Objects::nonNull)
.distinct()
.forEach(item -> getProcessedItem(item,anotherParam)
.ifPresent(result -> resultMap.put(item result));
return resultMap.build();

Java Streams: group a List into a Map of Maps

How could I do the following with Java Streams?
Let's say I have the following classes:
class Foo {
Bar b;
}
class Bar {
String id;
String date;
}
I have a List<Foo> and I want to convert it to a Map <Foo.b.id, Map<Foo.b.date, Foo>. I.e: group first by the Foo.b.id and then by Foo.b.date.
I'm struggling with the following 2-step approach, but the second one doesn't even compile:
Map<String, List<Foo>> groupById =
myList
.stream()
.collect(
Collectors.groupingBy(
foo -> foo.getBar().getId()
)
);
Map<String, Map<String, Foo>> output = groupById.entrySet()
.stream()
.map(
entry -> entry.getKey(),
entry -> entry.getValue()
.stream()
.collect(
Collectors.groupingBy(
bar -> bar.getDate()
)
)
);
Thanks in advance.
You can group your data in one go assuming there are only distinct Foo:
Map<String, Map<String, Foo>> map = list.stream()
.collect(Collectors.groupingBy(f -> f.b.id,
Collectors.toMap(f -> f.b.date, Function.identity())));
Saving some characters by using static imports:
Map<String, Map<String, Foo>> map = list.stream()
.collect(groupingBy(f -> f.b.id, toMap(f -> f.b.date, identity())));
Suppose (b.id, b.date) pairs are distinct. If so,
in second step you don't need grouping, just collecting to Map where key is foo.b.date and value is foo itself:
Map<String, Map<String, Foo>> map =
myList.stream()
.collect(Collectors.groupingBy(f -> f.b.id)) // map {Foo.b.id -> List<Foo>}
.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), // id
e -> e.getValue().stream() // stream of foos
.collect(Collectors.toMap(f -> f.b.date,
f -> f))));
Or even more simple:
Map<String, Map<String, Foo>> map =
myList.stream()
.collect(Collectors.groupingBy(f -> f.b.id,
Collectors.toMap(f -> f.b.date,
f -> f)));
An alternative is to support the equality contract on your key, Bar:
class Bar {
String id;
String date;
public boolean equals(Object o){
if (o == null) return false;
if (!o.getClass().equals(getClass())) return false;
Bar other = (Bar)o;
return Objects.equals(o.id, id) && Objects.equals(o.date, date);
}
public int hashCode(){
return id.hashCode*31 + date.hashCode;
}
}
Now you can just have a Map<Bar, Foo>.

Categories