Convert List of Maps to a single Map with VAVR - java

I'm struggling to reduce a List of Maps to a single Map using Vavr
<List<Map<String, Object>>>
to
<Map<String, Object>>
I tried it with flatmap/reduce/merge but unfortunately with no happy end :-(
Can someone provided an example how to do it with io.vavr.collection.List/io.vavr.collection.Map ?

You have several options.
For instance, given the following data set (sorry for its simplicity):
Map<String, Object> map1 = HashMap.of("key1", "value1", "key2", "value2");
Map<String, Object> map2 = HashMap.of("key3", "value3", "key4", "value4");
Map<String, Object> map3 = HashMap.of("key5", "value5", "key6", "value6");
List<Map<String, Object>> list = List.of(map1, map2, map3);
You can combine the different Maps with fold, for instance:
Map<String, Object> result = list.fold(HashMap.empty(), (m1, m2) -> m1.merge(m2));
You can use reduce as well:
Map<String, Object> result = list.reduce((m1, m2) -> m1.merge(m2));
Both of them use the Map merge method. There is an overloaded version of this method that allows you to define how collisions should be resolved.

I have no idea what vavr has to do with this.
list.stream() // A
.flatMap(map -> map.entrySet().stream()) // B
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
[A] Now you have a stream of Map<String, Object>.
[B] Now you have a stream of map entries.
Now you have a stream of key/value pairs.
Then just collect them into a map by providing a function to extract a key from a Map.Entry object, and a function that extracts a value.
Note that if you have a map of type Map<String, Object>, I give it 99% odds you're doing something wrong. Java is nominally, statically, and strongly typed, after all.

Related

Mapping map values in Java using streams

I have a map, Map<String, Map<String, String>> myMap = new HashMap<>(); that I would like to remap to get it's values, so that I get as a result Map<String, String>.
Is it possible to do the mapping using stream API?
I have solved the problem using a for loop but I'm interested if that could be done using streams.
My solution:
Map<String, String> result = new HashMap<>();
myMap.forEach((k, v) -> {
result.putAll(v);
});
What I want is to get all the values from myMap and put them in a new Map.
If you are certain there are no duplicate keys, you can do it like this.
Map<String, String> res = myMap.values()
.stream()
.flatMap(value -> value.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue);
If there may be duplicate keys between the inner maps, you will have to introduce merge function to resolve conflicts. Simple resolution keeping the value of the second encountered entry may look like this:
Map<String, String> res = myMap.values()
.stream()
.flatMap(value -> value.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2));
Basically, stream the values, which are Maps, flatten them to a stream of entries and collect the entries in a new Map.
You need to flatten the entries of the nested maps which can be done using either flatMap() or mapMulty().
And then apply collect() with the minimalistic two-args flavor of Collector toMap() passed as an argument. It would be sufficient since you don't expect duplicates.
Here's an example using flatMap():
Map<String, Map<String, String>> myMap = new HashMap<>();
Map<String, String> res = myMap.entrySet().stream() // stream of maps
.flatMap(entry -> entry.getValue().entrySet().stream()) // stream of map entries
.collect(Collectors.toMap(
Map.Entry::getKey, // key mapper
Map.Entry::getValue // value mapper
));
Example with Java 16 mapMulti() used for flattening the data:
Map<String, Map<String, String>> myMap = new HashMap<>();
Map<String, String> res = myMap.entrySet().stream() // stream of maps
.<Map.Entry<String, String>>mapMulti((entry, consumer) ->
entry.getValue().entrySet().forEach(consumer) // stream of map entries
)
.collect(Collectors.toMap(
Map.Entry::getKey, // key mapper
Map.Entry::getValue // value mapper
));

What is the easiest way to change the inner value type in a nested map in java?

I have a nested map Map<String, Map<String, List<ObjectA>>> passed to me, and I want to change it to type Map<String, Map<String, Set<ObjectA>>>, what is the easiest way to do so in Java using stream? I have tried to use Collectors.groupingBy but can't get it working.
The best way is you have to iterate through each entry in outer map and inner map, and then convert the inner map entry value List<ObjectA> to Set<ObjectA>
Map<String, Map<String, Set<ObjectA>>> resultMap = map.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, val -> new HashSet<>(val.getValue())))));
Note : If you are converting List to HashSet then you will not maintain same order, so you can choose LinkedHashSet over HashSet to maintain order
Map<String, Map<String, Set<ObjectA>>> resultMap = map.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, val -> new LinkedHashSet<>(val.getValue())))));

Efficient way to iterate and copy the values of HashMap

I want to convert:
Map<String, Map<String, List<Map<String, String>>>> inputMap
to:
Map<String, Map<String, CustomObject>> customMap
inputMap is provided in the config and is ready but I need to customMap Format. CustomObject will be derived from List<Map<String, String>> using few lines of code in a function.
I have tried a normal way of iterating input map and copying key values in customMap. Is there any efficient way of doing that using Java 8 or some other shortcut?
Map<String, Map<String, List<Map<String, String>>>> configuredMap = new HashMap<>();
Map<String, Map<String, CustomObj>> finalMap = new HashMap<>();
for (Map.Entry<String, Map<String, List<Map<String, String>>>> attributeEntry : configuredMap.entrySet()) {
Map<String, CustomObj> innerMap = new HashMap<>();
for (Map.Entry<String, List<Map<String, String>>> valueEntry : attributeEntry.getValue().entrySet()) {
innerMap.put(valueEntry.getKey(), getCustomeObj(valueEntry.getValue()));
}
finalMap.put(attributeEntry.getKey(), innerMap);
}
private CustomObj getCustomeObj(List<Map<String, String>> list) {
return new CustomObj();
}
One solution is to stream the entrySet of inputMap, and then use Collectors#toMap twice (once for the outer Map, and once for the inner Map):
Map<String, Map<String, CustomObj>> customMap = inputMap.entrySet()
.stream()
.collect(Collectors.toMap(Function.identity(), entry -> {
return entry.getValue()
.entrySet()
.stream()
.collect(Collectors.toMap(Function.identity(),
entry -> getCustomeObj(entry.getValue())));
}));
You could stream, but that ain't going to look readable; at least to me. So if you have a method:
static CustomObject fun(List<Map<String, String>> in) {
return .... // whatever processing you have here
}
you could still use the java-8 syntax, but in a different form:
Map<String, Map<String, CustomObject>> customMap = new HashMap<>();
inputMap.forEach((key, value) -> {
value.forEach((innerKey, listOfMaps) -> {
Map<String, CustomObject> innerMap = new HashMap<>();
innerMap.put(innerKey, fun(listOfMaps));
customMap.put(key, innerMap);
});
});
If you can make the inner map immutable, you could make that even shorter:
inputMap.forEach((key, value) -> {
value.forEach((innerKey, listOfMaps) -> {
customMap.put(key, Collections.singletonMap(innerKey, fun(listOfMaps)));
});
});
IMHO streaming is not so bad idea. There're no bad tools. It depends on how you're using them.
In this particular case I would extract the repeating pattern into an utility method:
public static <K, V1, V2> Map<K, V2> transformValues(Map<K, V1> map, Function<V1, V2> transformer) {
return map.entrySet()
.stream()
.collect(toMap(Entry::getKey, e -> transformer.apply(e.getValue())));
}
The method above can be implemented using any approach, though I think Stream API fits pretty well here.
Once you defined the utility method, it can be used as simple as follows:
Map<String, Map<String, CustomObj>> customMap =
transformValues(inputMap, attr -> transformValues(attr, this::getCustomObj));
The actual transformation is effectively one liner. So with proper JavaDoc for transformValues method the result code is pretty readable and maintainable.
How about Collectors.toMap for the entries both at an outer and inner level such as:
Map<String, Map<String, CustomObj>> finalMap = configuredMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
attributeEntry -> attributeEntry.getValue().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
valueEntry -> getCustomeObj(valueEntry.getValue())))));

Merge two Maps (Map<String, Map<String, Object>>) with duplicate keys

I have two ImmutableMaps from guava that I am trying to combine them where they can have duplicate keys;
Map<String, Map<String, Object>> map1 = new ImmutableMap.Builder<String, Map<String, Object>>()
.put("config", ImmutableMap.of(
"key1", "value1",
"key2", "value2"))
.put("categoryapproval", ImmutableMap.of("reason_code", "listing_quality"))
.build();
Map<String, Map<String, Object>> map2 = new ImmutableMap.Builder<String, Map<String, Object>>()
.put("config", ImmutableMap.of(
"key1", "value3",
"key4", "value4"))
.build();
So, I can not use putAll() method because it throws DuplicateKeyException which is as expected. The output I am trying to get is like;
"config" --> "key1": {value1, value3},
"key2": {value2},
"key4": {value4}
Finally, I have also tried MultiValueMap, however, MultiValueMap keeps values as List where I need to iterate over. In map1 I can get value1 by map1.get("config").get("key1") where value1 can be any kind of object. I appreciate for any kind of help.
You can use Guava's Multimap and Java 8's Map.merge(K, V, BiFunction):
Map<String, Multimap<String, Object>> merged = new HashMap<>();
BiFunction<Multimap<String, Object>, Multimap<String, Object>, Multimap<String, Object>> remappingFunction = (value1, value2) -> {
Multimap<String, Object> multimap = HashMultimap.<String, Object>create();
multimap.putAll(value1);
multimap.putAll(value2);
return multimap;
};
map1.forEach((key, value) -> merged.merge(key, Multimaps.forMap(value), remappingFunction));
map2.forEach((key, value) -> merged.merge(key, Multimaps.forMap(value), remappingFunction));
merged.get("config").get("key1");
If you are not using Java 8 then you'll need to manage the merging of multimaps in some other way but the idea is the same.

How do I transform a Map<K,V1> to another Map<K,V2> using a map of functions in Java 8?

I want to transform a Map<String, Integer> to another Map<String, Long> using a map of functions in Java 8, matching the data with the functions by key in both maps. You can assume that both maps have the same keys.
I tried the following approach:
Map<String, Integer> inputData = new HashMap<>();
inputData.put("A",8);
inputData.put("B",7);
inputData.put("C",6);
Map<String, Function<Integer, Long>> transformers = new HashMap<>();
transformers.put("A", x -> x*2L);
transformers.put("B", x -> x+3L);
transformers.put("C", x -> x+11L);
Map<String, Long> mappedData = inputData.entrySet().stream()
.map(entry -> new AbstractMap.SimpleEntry<>(
entry.getKey(),
transformers.get(entry.getKey()).apply(entry.getValue())))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
Expected result: {A=16, B=10, C=17}.
Is there any simpler way of expressing "apply map of transformers to the map of inputData matching by key" in Java Streams API?
You may transform directly in the collector:
Map<String, Long> mappedData = inputData.entrySet().stream()
.collect(toMap(Map.Entry::getKey,
entry -> transformers.get(entry.getKey()).apply(entry.getValue())));
Such solution is shorter and does not create intermediate objects.
Were your inputData had the same value type (Map<String, Long>), you could perform the transformation in-place:
inputData.replaceAll((key, value) -> transformers.get(key).apply(value));
You can also start from transformers map itself, which looks slightly easier:
Map<String, Long> collect = transformers.entrySet().stream()
.collect(toMap(Map.Entry::getKey,
e -> e.getValue().apply(inputData.get(e.getKey()))));
If one extra line does not bother you, try
Map<String, Long> result = new HashMap<>(); // this line
inputData.forEach((k, v) -> result.put(k, transformers.get(k).apply(v));

Categories

Resources