Java Streams: group a List into a Map of Maps - java

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>.

Related

How to use a stream to create a map?

Input: List<Foo> rawdata
Desired Output: Map<Foo, Bar>
Method implementation: Java 8
public Map<Foo, Bar> getMap(List<Foo> rawdata) {
rawData.stream().flatmap(d -> {
val key = method1(d);
val value = method2(d);
})
// Now I want to create a map using the Key/Value
// pair which was obtained from calling method 1 and 2.
}
I did try this:
public Map<Foo, Bar> getMap(List<Foo> rawdata) {
rawData.stream()
.flatmap(
d -> {
val key = method1(d);
val value = method2(d);
return Stream.of(new Object[][]{key, value});
})
.collect(Collectors.toMap(
key -> key[0],
key -> key[1]));
}
However want to see if there is less expensive way of doing this or any better way.
There's no point in creating an intermediate stream and then using flatMap().
You can just collect() directly into a map:
public Map<Foo, Bar> getMap(List<Foo> rawData) {
return rawData.stream()
.collect(Collectors.toMap(foo -> method1(foo), foo -> method2(foo)));
// OR: Collectors.toMap(this::method1, this::method2);
}
You can use toMap(keyMapper,valueMapper,mergeFunction) method with three parameters that allows to merge duplicates into a list, for example:
List<String[]> list = List.of(
new String[]{"Foo1", "Bar1"},
new String[]{"Foo1", "Bar2"},
new String[]{"Foo2", "Baz"});
Map<String, List<String>> map1 = list.stream()
.collect(Collectors.toMap(
e -> e[0],
e -> new ArrayList<>(List.of(e[1])),
(list1, list2) -> {
list1.addAll(list2);
return list1;
}));
System.out.println(map1);
// {Foo1=[Bar1, Bar2], Foo2=[Baz]}
If you are sure that there are no duplicates, you can use toMap(keyMapper,valueMapper) method with two parameters:
List<String[]> list = List.of(
new String[]{"Foo1", "Bar1"},
new String[]{"Foo2", "Baz"});
Map<String, String> map2 = list.stream()
.collect(Collectors.toMap(e -> e[0], e -> e[1]));
System.out.println(map2);
// {Foo1=Bar1, Foo2=Baz}
Anyway you can collect a list of Map.Entrys:
List<String[]> list = List.of(
new String[]{"Foo1", "Bar1"},
new String[]{"Foo1", "Bar2"},
new String[]{"Foo2", "Baz"});
List<Map.Entry<String, String>> list1 = list.stream()
.map(arr -> Map.entry(arr[0], arr[1]))
.collect(Collectors.toList());
System.out.println(list1);
// [Foo1=Bar1, Foo1=Bar2, Foo2=Baz]
See also: Ordering Map<String, Integer> by List<String> using streams

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;
}

How create HashMap from flatMap?

I'm have two Maps in method param.
private Map<String, List<Attr>> getPropAttr(Map<String, List<Attr>> redundantProperty,
Map<String, List<Attr>> notEnoughProperty) {
Map<String, List<Attr>> propAttr = new HashMap<>();
redundantProperty.forEach((secondPropertyName, secondPropertyAttributes) -> notEnoughProperty.entrySet().stream()
.filter(firstPropertyName -> secondPropertyName.contains(firstPropertyName.getKey()))
.forEach(firstProperty -> {
List<Attr> firstPropertyAttrs = firstProperty.getValue();
List<Attr> redundantPropAttrs = getRedundantPropAttrs(secondPropertyAttrs, firstPropertyAttrs);
String propName = firstProperty.getKey();
propAttr.put(propertyName, redundantPropAttrs);
}));
return propAttr;
I want to rewrite this method on stream. But, i have some problems in stream collectors. It's don't see return value(List ) from stream into flatmap. In below - my attempt to rewrite this method on stream API. How set second param in collect(toMap(first::get, second::get))?
Thank you an advance.
private Map<String, List<Attr>> getPropAttr(Map<String, List<Attr>> redundantProperty,
Map<String, List<Attr>> notEnoughProperty) {
return redundantProperty.entrySet().stream()
.flatMap(secondProperty -> notEnoughProperty.entrySet().stream()
.filter(firstPropertyName -> secondProperty.getKey().contains(firstPropertyName.getKey()))
.map(firstProperty -> {
List<Attr> onlinePropertyAttrs = firstProperty.getValue();
List<Attr> redundantPropAttrs =
getRedundantPropAttrs(secondProperty.getValue(), firstPropertyAttrs);
return redundantPropertyAttrs;
}))
.collect(toMap(Property::getName, toList()));
After your flatMap call, your Stream becomes a Stream<List<Attr>>. It looks like you lose the property you want to use as a key for the output Map at this point.
Instead, I suggest that the map inside the flatMap return a Map.Entry containing the required key and value:
return redundantProperty.entrySet()
.stream()
.flatMap(secondProperty ->
notEnoughProperty.entrySet()
.stream()
.filter(firstPropertyName -> secondProperty.getKey().contains(firstPropertyName.getKey()))
.map(firstProperty -> {
List<Attr> redundantPropAttrs = ...
...
return new SimpleEntry<String,List<Attr>>(firstProperty.getKey(),redundantPropertyAttrs);
}))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

Working with nested maps using Stream API

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.

Java 8 : grouping field values by field names

I'm trying to find a more elegant way to create a map that group field values by field names using Java 8 than the following:
#Test
public void groupFieldValuesByFieldNames() {
Person lawrence = aPerson().withFirstName("Lawrence").withLastName("Warren").born();
Person gracie = aPerson().withFirstName("Gracie").withLastName("Ness").born();
Map<String, List<String>> valuesByFieldNames = new HashMap<>();
Stream.of(lawrence, gracie).forEach(person -> {
valuesByFieldNames.computeIfAbsent("lastName", s -> new ArrayList<>()).add(person.getLastName());
valuesByFieldNames.computeIfAbsent("firstName", s -> new ArrayList<>()).add(person.getFirstName());
});
assertThat(valuesByFieldNames, hasEntry("lastName", asList("Warren", "Ness")));
assertThat(valuesByFieldNames, hasEntry("firstName", asList("Lawrence", "Gracie")));
}
Try this.
Map<String, List<String>> valuesByFieldNames = Stream.of(lawrence, gracie)
.flatMap(p -> Stream.of(new String[]{"firstName", p.getFirstName()},
new String[]{"lastName", p.getLastName()}))
.collect(Collectors.groupingBy(a -> a[0],
Collectors.mapping(a -> a[1], Collectors.toList())));
Or more generally
Map<String, List<String>> valuesByFieldNames = Stream.of(lawrence, gracie)
.flatMap(p -> Stream.of(new AbstractMap.SimpleEntry<>("firstName", p.getFirstName()),
new AbstractMap.SimpleEntry<>("lastName", p.getLastName())))
.collect(Collectors.groupingBy(e -> e.getKey(),
Collectors.mapping(e -> e.getValue(), Collectors.toList())));
You can have the following, that will work correctly in parallel:
Map<String, List<String>> valuesByFieldNames =
Stream.of(lawrence, gracie).collect(HashMap::new, (m, p) -> {
m.computeIfAbsent("lastName", s -> new ArrayList<>()).add(p.getLastName());
m.computeIfAbsent("firstName", s -> new ArrayList<>()).add(p.getFirstName());
}, (m1, m2) -> m2.forEach((k, v) -> m1.merge(k, v, (l1, l2) -> { l1.addAll(l2); return l1; })));
What this does is that it collect each person into a mutable HashMap. The accumulator computes the last name and the first name by invoking computeIfAbsent, just like your initial code. The combiner merges two maps together by iterating over the entries of the second map and merging each key into the first map; in case of conflict, the value is the addition of the two lists.

Categories