Merging Map streams using Java 8 Lambda Expression - java

I have two maps m1 and m2 of type Map<Integer, String>, which has to be merged into a single map
Map<Integer, List<String>>, where values of same keys in both the maps are collected into a List and put into a new Map.
Solution based on what I explored:
Map<Integer, List<String>> collated =
Stream.concat(m1.entrySet().stream(), m2.entrySet().stream()).collect(
Collectors.toMap(Entry::getKey,
Entry::getValue, (a, b) -> {
List<String> merged = new ArrayList<String>(a);
merged.addAll(b);
return merged;
}));
But, this solution expects source List to be Map<Integer, List<String>> as the merge function in toMap expects operands and result to be of the same type.
I don't want to change the source collection. Please provide your inputs on achieving this using lambda expression.

That's a job for groupingBycollector:
Stream.of(m1,m2)
.flatMap(m->m.entrySet().stream())
.collect(groupingBy(
Map.Entry::getKey,
mapping(Map.Entry::getValue, toList())
));

I can't test it right now, but I think all you need it to change the mapping of the value from Entry::getValue to a List that contains that value :
Map<Integer, List<String>> collated =
Stream.concat(m1.entrySet().stream(), m2.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey,
e -> {
List<String> v = new ArrayList<String>();
v.add(e.getValue());
return v;
},
(a, b) -> {
List<String> merged = new ArrayList<String>(a);
merged.addAll(b);
return merged;
}));
EDIT: The idea was correct. The syntax wasn't. Current syntax works, though a bit ugly. There must be a shorter way to write it.
You can also replace e -> {..} with e -> new ArrayList<String>(Arrays.asList(new String[]{e.getValue()})).
or with e -> Stream.of(e.getValue()).collect(Collectors.toList())
Or you can do it with groupingBy :
Map<Integer, List<String>> collated =
Stream.concat(m1.entrySet().stream(), m2.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue,
Collectors.toList())));

Misha's solution is the best if you want pure Java-8 solution. If you don't mind using third-party libraries, it would be a little shorter using my StreamEx.
Map<Integer, List<String>> map = StreamEx.of(m1, m2)
.flatMapToEntry(Function.identity())
.grouping();
Internally it's the same as in Misha's solution, just syntactic sugar.

This seems like a great opportunity to use Guava's Multimap.
ListMultimap<Integer, String> collated = ArrayListMultimap.create();
collated.putAll(Multimaps.forMap(m1));
collated.putAll(Multimaps.forMap(m2));
And if you really need a Map<Integer, List<String>>:
Map<Integer, List<String>> mapCollated = Multimaps.asMap(collated);

You can use the Collectors.toMap method with three parameters for clarity.
Map<Integer, String> m1 = Map.of(1, "A", 2, "B");
Map<Integer, String> m2 = Map.of(1, "C", 2, "D");
// two maps into one
Map<Integer, List<String>> m3 = Stream
.of(m1.entrySet(), m2.entrySet())
.flatMap(Collection::stream)
.collect(Collectors.toMap(
// key - Integer
e -> e.getKey(),
// value - List<String>
e -> List.of(e.getValue()),
// mergeFunction - two lists into one
(list1, list2) -> {
List<String> list = new ArrayList<>();
list.addAll(list1);
list.addAll(list2);
return list;
}));
// output
System.out.println(m3); // {1=[A, C], 2=[B, D]}

Related

Java 8 stream grouping a List<Map<>> by the same <Key, Value> to a new List<Map<>>

I have a List<Map<String,String>>
such as:
Map<String, String> m1 = new HashMap<>();
m1.put("date", "2020.1.5");
m1.put("B", "10");
Map<String, String> m2 = new HashMap<>();
m2.put("date", "2020.1.5");
m2.put("A", "20");
Map<String, String> m3 = new HashMap<>();
m3.put("date", "2020.1.6");
m3.put("A", "30");
Map<String, String> m4 = new HashMap<>();
m4.put("date", "2020.1.7");
m4.put("C", "30");
List<Map<String, String>> before = new ArrayList<>();
before.add(m1);
before.add(m2);
before.add(m3);
before.add(m4);
My expect result is to generate a new List map, which is grouped by date , and all the entry set in the same date would be put together, like:
[{"A":"20","B":"10","date":"2020.1.5"},{"A":"30","date":"2020.1.6"},{"C":"30","date":"2020.1.7"}]
I tried with the following method, but always not my expect result.
stream().flatmap().collect(Collectors.groupingBy())
Some Additional Comments for this problem:
I worked this out with for LOOP, but the application hangs when the list size is about 50000, so I seek a better performant way to do this. Java 8 stream flat map is a perhaps way as far as I know.
So the key point is not only to remap this but also with the most performant way to do this.
before
.stream()
.collect(Collectors.toMap((m) -> m.get("date"), m -> m, (a,b) -> {
Map<String, String> res = new HashMap<>();
res.putAll(a);
res.putAll(b);
return res;
}))
.values();
This is the solution you're looking for.
The toMap function receives 3 parameters:
the key mapper, which in your case is the date
the value mapper, which is the map itself that's being processed
the merge function, which takes 2 maps with the same date and puts all the keys together
Output:
[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]
You can do this way using groupingBy and Collector.of
List<Map<String, String>> list = new ArrayList<>(before.stream()
.collect(Collectors.groupingBy(
k -> k.get("date"),
Collector.of( HashMap<String,String>::new,
(m,e)-> m.putAll(e),
(map1,map2)->{ map1.putAll(map2); return map1;}
))).values());
Here, first use Collectors.groupingBy to group by date. Then define custom collector using Collector.of to collect List<Map<String, String>> into Map<String, String>. After create list using map values.
And using Collectors.flatMapping from Java 9
List<Map<String, String>> list = new ArrayList<>(before.stream()
.collect(Collectors.groupingBy(
k -> k.get("date"),
Collectors.flatMapping(m -> m.entrySet().stream(),
Collectors.toMap(k -> k.getKey(), v -> v.getValue(), (a,b) -> a))))
.values());
You can achieve the very same result using a certain number of Collectors, orderly:
Collectors.groupingBy to group by the date
Collectors.reducing to merge the Map<String, String> items
Collectors.collectingAndThen to transform the values from Map<String, Optional<Map<String, String>>>, as a result of the previous reducing to the final output List<Map<String, String>>.
List<Map<String, String>> list = before.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
m -> m.get("date"),
Collectors.reducing((l, r) -> {
l.putAll(r);
return l; })
),
o -> o.values().stream()
.flatMap(Optional::stream)
.collect(Collectors.toList())));
The list contains what are you looking for:
[{date=2020.1.5, A=20, B=10}, {date=2020.1.6, A=30}, {date=2020.1.7, C=30}]
Important: This solution has two he disadvantages:
It looks clumsy and might not be clear for an independent viewer
It mutates (modifies) the original maps included in the List<Map<String, String>> before.
It can be done as follows:
List<Map<String, String>> remapped = before.stream()
.collect(Collectors.groupingBy(m -> m.get("date")))
.values().stream()
.map(e -> e.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (x1, x2) -> x1)))
.collect(Collectors.toList());
remapped.forEach(System.out::println);
Output:
{date=2020.1.5, A=20, B=10}
{date=2020.1.6, A=30}
{date=2020.1.7, C=30}

Java8 convert List of Map to List of string

I am using Java8 to achieve the below things,
Map<String, String> m0 = new HashMap<>();
m0.put("x", "123");
m0.put("y", "456");
m0.put("z", "789");
Map<String, String> m1 = new HashMap<>();
m1.put("x", "000");
m1.put("y", "111");
m1.put("z", "222");
List<Map<String, String>> l = new ArrayList<>(Arrays.asList(m0, m1));
List<String> desiredKeys = Lists.newArrayList("x");
List<Map<String, String>> transformed = l.stream().map(map -> map.entrySet().stream()
.filter(e -> desiredKeys.stream().anyMatch(k -> k.equals(e.getKey())))
.collect(Collectors.toMap(e -> e.getKey(), p -> p.getValue())))
.filter(m -> !m.isEmpty())
.collect(Collectors.toList());
System.err.println(l);
System.err.println(transformed);
List<String> values = new ArrayList<>();
for (Map<String,String> map : transformed) {
values.add(map.values().toString());
System.out.println("Values inside map::"+map.values());
}
System.out.println("values::"+values); //values::[[123], [000]]
Here, I would like to fetch only the x-values from the list. I have achieved it but it is not in a proper format.
Expected output:
values::[123, 000]
Actual output:
values::[[123], [000]]
I know how to fix the actual output. But is there any easy way to achieve this issue? Any help would be appreciable.
You do not need to iterate over the entire map to find an entry by its key. That's what Map.get is for. To flatten the list of list of values, use flatMap:
import static java.util.stream.Collectors.toList;
.....
List<String> values = l.stream()
.flatMap(x -> desiredKeys.stream()
.filter(x::containsKey)
.map(x::get)
).collect(toList());
On a side note, avoid using l (lower case L) as a variable name. It looks too much like the number 1.
I’m not sure Streams will help, here. It’s easier to just loop through the Maps:
Collection<String> values = new ArrayList<>();
for (Map<String, String> map : l) {
Map<String, String> copy = new HashMap<>(map);
copy.keySet().retainAll(desiredKeys);
values.addAll(copy.values());
}
Flat map over the stream of maps to get a single stream representing the map entries of all your input maps. From there, you can filter out each entry whose key is not contained in the desired keys. Finally, extract the equivalent value of each entry to collect them into a list.
final List<String> desiredValues = l.stream()
.map(Map::entrySet)
.flatMap(Collection::stream)
.filter(entry -> desiredKeys.contains(entry.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
EDIT
This assumes that if a map has the key "x" it must also has the key "y" so to fetch the corredponding value.
final List<String> desiredValues = l.stream()
.filter(map -> map.containsKey("x"))
.map(map -> map.get("y"))
.collect(Collectors.toList());

Java-Stream, toMap with duplicate keys

So there might be one abc for several payments, now I have:
//find abc id for each payment id
Map<Long, Integer> abcIdToPmtId = paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(Collectors.toMap(Payment::getAbcId, Payment::getPaymentId));
But then I reallize this could have duplicate keys, so I want it to return a
Map<Long, List<Integer>> abcIdToPmtIds
which an entry will contain one abc and his several payments.
I know I might can use groupingBy but then I think I can only get Map<Long, List<Payments>> .
Use the other groupingBy overload.
paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(
groupingBy(Payment::getAbcId, mapping(Payment::getPaymentId, toList());
Problem statement: Converting SimpleImmutableEntry<String, List<String>> -> Map<String, List<String>>.
For Instance you have a SimpleImmutableEntry of this form [A,[1]], [B,[2]], [A, [3]] and you want your map to looks like this: A -> [1,3] , B -> [2].
This can be done with Collectors.toMap but Collectors.toMap works only with unique keys unless you provide a merge function to resolve the collision as said in java docs.
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-
So the example code looks like this:
.map(returnSimpleImmutableEntries)
.collect(Collectors.toMap(SimpleImmutableEntry::getKey,
SimpleImmutableEntry::getValue,
(oldList, newList) -> { oldList.addAll(newList); return oldList; } ));
returnSimpleImmutableEntries method returns you entries of the form [A,[1]], [B,[2]], [A, [3]] on which you can use your collectors.
With Collectors.toMap:
Map<Long, Integer> abcIdToPmtId = paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(Collectors.toMap(
Payment::getAbcId,
p -> new ArrayList<>(Arrays.asList(p.getPaymentId())),
(o, n) -> { o.addAll(n); return o; }));
Though it's more clear and readable to use Collectors.groupingBy along with Collectors.mapping.
You don't need streams to do it though:
Map<Long, Integer> abcIdToPmtId = new HashMap<>();
paymentController.findPaymentsByIds(pmtIds).forEach(p ->
abcIdToPmtId.computeIfAbsent(
p.getAbcId(),
k -> new ArrayList<>())
.add(p.getPaymentId()));

convert a Map<String, List<MyObject> to List<Map<String, MyObject>> in java

I want to convert a Map<String, List<MyObject>> to List<Map<String, MyObject>>
{<key1,[myObject1, myObject2]>, <key2,[myObject3, myObject4]>}
will be converted to
[{<key1,myObject1>, <key2,myObject3>}, {<key1,myObject2>, <key2, myObject4>}]
where myObject1 and myObject3 have a same unique id and so do myObject2 and myObject4.
my implementation is below but is there a more optimal way of doing this.
private List<Map<String, MyObject>> getObjectMapList( Map<String, List<MyObject>> objectMap)
{
List<Map<String, MyObject>> objectMapList = new ArrayList<Map<String,MyObject>>();
for(MyObject myObject : objectMap.get("key1")) {// there will be default key1 whose value is known
Map<String, MyObject> newMap= new HashMap<String, MyObject>();
for (String key : objectMap.keySet()) {
newMap.put(key, objectMap.get(key).stream()
.filter(thisObject -> thisObject.getId().equals(myObject.getId()))
.collect(Collectors.toList()).get(0));
}
objectMapList.add(newMap);
}
return objectMapList;
}
Here's a 1-liner without any curly brackets:
private List<Map<String, MyObject>> getObjectMapList( Map<String, List<MyObject>> objectMap) {
return map.entrySet().stream()
.map(e -> e.getValue().stream()
.map(o -> Collections.singletonMap(e.getKey(), o))
.collect(Collections.toList())
.flatMap(List::stream)
.collect(Collections.toList());
}
The main "trick" here is the use of Collections.singletonMap() to allow a blockless in-line create-and-populate of a map.
Disclaimer: Code may not compile or work as it was thumbed in on my phone (but there's a reasonable chance it will work)
This stream should return you the desired result. With my old Eclipse version, I had some trouble with types. You might have to break it up into single steps, or add some types in the lambdas, but I wanted to keep it short.
Map<String, List<MyObject>> objectMap = new HashMap<>();
objectMap.keySet()
.stream()
.flatMap(key -> objectMap.get(key)
.stream()
.map(obj -> new AbstractMap.SimpleEntry<>(key, obj)))
.collect(groupingBy(pair -> pair.getValue().getId()))
.values()
.stream()
.map(listOfSameIds -> listOfSameIds.stream()
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
.collect(toList());
What I do is:
Pair all the objects in all your input's values with their keys and put them in one long list (flatMap(key -> streamOfKeyObjectPairs)).
Separate those pairs by the IDs of the objects (collect(groupingBy)).
Take each of those groups and convert the lists of pairs into maps (map(list -> toMap))
Put all those maps into a list

Merge two List value maps

Anybody knows how to merge with Java 8 two maps of this type?
Map<String, List<String>> map1--->["a",{1,2,3}]
Map<String, List<String>> map2--->["a",{4,5,6}]
And obtain as result of the merge
Map<String, List<String>> map3--->["a",{1,2,3,4,5,6}]
I´m looking for a non verbose way if exist. I know how to do it in the old fashion way.
Regards.
The general idea is the same as in this post. You create a new map from the first map, iterate over the second map and merge each key with the first map thanks to merge(key, value, remappingFunction). In case of conflict, the remapping function is applied: in this case, it takes the two lists and merges them; if there is no conflict, the entry with the given key and value is put.
Map<String, List<String>> mx = new HashMap<>(map1);
map2.forEach((k, v) -> mx.merge(k, v, (l1, l2) -> {
List<String> l = new ArrayList<>(l1);
l.addAll(l2);
return l;
}));
You could try this, which gradually flattens the structure until you have a stream of tuples of the maps keys versus the lists values:
Map<K,List<V>> result = Stream.of(map1,map2) // Stream<Map<K,List<V>>>
.flatMap(m -> m.entrySet().stream()) // Stream<Map.Entry<K,List<V>>>
.flatMap(e -> e.getValue().stream() // Inner Stream<V>...
.map(v -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
// ...flatmapped into an outer Stream<Map.Entry<K,V>>>
.collect(Collectors.groupingBy(e -> e.getKey(), Collectors.mapping(e -> e.getValue(), Collectors.toList())));
Another option would avoid the internal streaming of the lists by using Collectors.reducing() as a second parameter of groupingBy, I guess. However, I would consider the accepted answer first
You have to use Set instead of List and can do it like this:
Map<String, Set<String>> map1--->["a",{1,2,3}]
Map<String, Set<String>> map2--->["a",{4,5,6}]
map1.forEach((k, v) -> v.addAll(map2.get(k) == null : new HashSet<> ? map2.get(k)));

Categories