Switching Keys in a Map of Maps - java

How would you use Java 8 streams to swap keys in this map of maps? Or at least clean up this mess a little bit...
Map<Type1, Map<Type2, String>> to Map<Type2, Map<Type1, String>>
Using nested for loops (untested):
Map<Type1, Map<Type2, String>> map ...
Map<Type2, Map<Type1, String>> map2 = new HashMap<>();
for (Type 1 type1 : map.keySet()) {
for(Entry<Type2, String> entry : map.get(type1)) {
if (map2.get(entry.key() == null) {
map2.push(entry.key(), new HashMap<Type1, String>();
}
map2.get(entry.key()).put(type1, entry.value();
}
}
So far I think you would need to flap map into all unique combinations of Type1, Type2, and String and store this set in some sort of intermediate collection.
Definitely wrong:
map.entrySet().stream().flatMap(t -> <Type1, Type2,
String>).collect(Collectors.toMap(t -> t.Type2, Collectors.toMap(t ->
t.type1, t->t.String))

Streams aren't well-suited for this type of problem. Instead, consider using other java 8 additions -- Map#forEach and Map#computeIfAbsent:
map.forEach( (t1, e) ->
e.forEach( (t2, v) ->
result.computeIfAbsent(t2, x -> new HashMap<>()).put(t1, v)
)
);

Misha already showed you the straight forward solution. If you really want to use Streams it could look like this:
public static <S, T> Map<T, Map<S, String>> convertStream(Map<S, Map<T, String>> map) {
return map.entrySet().stream().flatMap(m1 -> m1.getValue().entrySet()
.stream().map(e -> new Object() {
final T outer = e.getKey();
final Map<S, String> map;
{
map = new HashMap<>();
map.put(m1.getKey(), e.getValue());
}
})).collect(Collectors.toMap(o -> o.outer, o -> o.map, (m1, m2) -> {
m1.putAll(m2);
return m1;
}));
}

Map<Type2, Map<Type1, Object>> finalAnswer = map.entrySet().stream()
.collect(()->new HashMap<Type2,Map<Type1,Object>>(),
(mapAccumulator, left)->{
for(Entry<?, ?> leftEntry : left.getValue().entrySet() ){
Map<Type1,Object> tempMap = new HashMap<>();
tempMap.put(left.getKey(), leftEntry.getValue());
mapAccumulator.put((Type2) leftEntry.getKey(), tempMap);
}
/*accumulator*/},
(mapLeft,mapRight)->{mapLeft.putAll(mapRight); /*combiner*/});
map.entrySet().forEach(System.out::println);

Related

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())))));

Invert Map <String, List<String>> using Java 8

I need to invert map which is <String, List<String>> to Map<String,String> using java 8. with assumption that the values are unique. For example,
Input Map -
{"Fruit" -> ["apple","orange"], "Animal" -> ["Dog","Cat"]}
Output Map
{"apple" -> "Fruit", "orange" -> "Fruit", "Dog"->"Animal", "Cat" -> "Animal"}
Map <String, String> outputMap = new HashMap<>();
for (Map.Entry<String, List<String>> entry : inputMap.entrySet()) {
entry.getValue().forEach(value -> outputMap.put(value, entry.getKey()));
}
Is this right? can we achieve this using streams java 8?
You an try this way
Map <String, String> updatedMap = new HashMap<>();
oldMap.keySet()
.forEach(i -> oldMap.get(i)
.forEach(k -> updatedMap.put(k, i)));
Do like this :
public class InverterMap {
public static void main(String[] args) {
Map<String, List<String>> mp = new HashMap<String, List<String>>();
mp.put("Fruit", Arrays.asList("Apple", "Orange"));
mp.put("Animal", Arrays.asList("Dog", "Cat"));
System.out.println(mp); // It returned {Fruit=[Apple, Orange], Animal=[Dog, Cat]}
Map<String, String> invertMap = mp.entrySet().stream().collect(HashMap::new,
(m, v) -> v.getValue().forEach(k -> m.put(k, v.getKey())), Map::putAll);
System.out.println(invertMap);// It returned {Apple=Fruit, Cat=Animal, Orange=Fruit, Dog=Animal}
}
}
Read Stream.collect(Supplier supplier, BiConsumer, BiConsumer combiner) for more info.

Java8 Stream List<Map<String,Object>> groupingBy and counting value

i want get result like { "key1" : 4 ,"key2" :2 }
i known i can use map and groupby and such as
list.stream()
.map(map -> map2Entity(map))
.collect(Collectors.groupingBy(Entity::getKey,Collectors.summarizingInt(Entity::getCnt)) )
This is my code and how implements (todo) code
public void test() {
List<Map<String, Object>> list = Arrays.asList(
createNewMap("key1", 1),
createNewMap("key2", 2),
createNewMap("key1", 3)
);
// i want get result like {"key1":4,"key2":2}
// how can i get the result don't use map()
list.stream()
.collect(Collectors.groupingBy(this::getKey),....(todo));
}
private String getKey(Map<String,Object> map){
return (String) map.get("key");
}
private Map<String, Object> createNewMap(String key, Integer val) {
Map<String, Object> map = new HashMap<>();
map.put("key", key);
map.put(key, val);
return map;
}
You have to use the flatMap operator along with the groupingBy collector. Here's how it looks.
Map<String, Integer> keyToSumValuesMap = list.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.summingInt(Map.Entry::getValue)));
Moreover do not use Object type to represent integers since that is not type safe. Consider declaring method level generics to overcome that issue. Here's how it looks.
private static <S, T> Map<S, T> createNewMap(S key, T val) {
Map<S, T> map = new HashMap<>();
map.put(key, val);
return map;
}
And the output now looks like this:
{key1=4, key2=2}

Could you help me to merge values of several maps?

I'm trying to do the following modification:
final Map<String, List<Map<String, String>>> scopes = scopeService.fetchAndCacheScopesDetails();
final Map<String, Map<String, String>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> (String) s.get(SCOPE_NM), s -> (String) s.get(SCOPE_ID))))
);
But I face "Duplicate key" error, so I'd like to change scopeResponses to Map<String, Map<String, List<String>>>
Could you tell me how to merge values s -> (String) s.get(SCOPE_ID) into a List or Set in this situation?
You need to create a Set for the value of the inner Map, and supply a merge function:
final Map<String, Map<String, Set<String>>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> s.get(SCOPE_NM),
s -> {Set<String> set= new HashSet<>(); set.add(s.get(SCOPE_ID)); return set;},
(s1,s2)->{s1.addAll(s2);return s1;}))));
Or, you can construct the inner Map with groupingBy:
final Map<String, Map<String, Set<String>>> scopesResponse2 = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.groupingBy(s -> s.get(SCOPE_NM),
Collectors.mapping(s -> s.get(SCOPE_ID),Collectors.toSet())))));
You can also do it using Guava's ListMultimap (multimap is like a map of lists):
Map<String, ListMultimap<String, String>> scopesResponse = scopes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toMultimap(e)));
where
static ImmutableListMultimap<String, String> toMultimap(
Map.Entry<String, List<Map<String, String>>> entry) {
return entry.getValue().stream().collect(ImmutableListMultimap.toImmutableListMultimap(
s -> (String) s.get(SCOPE_NM),
s -> (String) s.get(SCOPE_ID)
));
}
If the values in the lists turn out to be duplicated, and you don't want that, use SetMultimap instead.

Merge list of maps using Java 8 Stream API

I'm trying to learn Java 8 Stream and when I try to convert some function to java8 to practice. I meet a problem.
I'm curious that how can I convert follow code to java stream format.
/*
* input example:
* [
{
"k1": { "kk1": 1, "kk2": 2},
"k2": {"kk1": 3, "kk2": 4}
}
{
"k1": { "kk1": 10, "kk2": 20},
"k2": {"kk1": 30, "kk2": 40}
}
]
* output:
* {
"k1": { "kk1": 11, "kk2": 22},
"k2": {"kk1": 33, "kk2": 44}
}
*
*
*/
private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) {
Set<String> keys_1 = valueList.get(0).keySet();
Set<String> keys_2 = valueList.get(0).entrySet().iterator().next().getValue().keySet();
Map<String, Map<String, Long>> result = new HashMap<>();
for (String k1: keys_1) {
result.put(k1, new HashMap<>());
for (String k2: keys_2) {
long total = 0;
for (Map<String, Map<String, Long>> mmap: valueList) {
Map<String, Long> m = mmap.get(k1);
if (m != null && m.get(k2) != null) {
total += m.get(k2);
}
}
result.get(k1).put(k2, total);
}
}
return result;
}
The trick here is to collect correctly the inner maps. The workflow would be:
Flat map the list of map List<Map<String, Map<String, Long>>> into a stream of map entries Stream<Map.Entry<String, Map<String, Long>>>.
Group by the key of each of those entry, and for the values mapped to same key, merge the two maps together.
Collecting maps by merging them would ideally warrant a flatMapping collector, which unfortunately doesn't exist in Java 8, although it will exist in Java 9 (see JDK-8071600). For Java 8, it is possible to use the one provided by the StreamEx library (and use MoreCollectors.flatMapping in the following code).
private static Map<String, Map<String, Long>> mergeMapsValue(List<Map<String, Map<String, Long>>> valueList) {
return valueList.stream()
.flatMap(e -> e.entrySet().stream())
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.flatMapping(
e -> e.getValue().entrySet().stream(),
Collectors.<Map.Entry<String,Long>,String,Long>toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum)
)
));
}
Without using this convenient collector, we can still build our own with equivalent semantics:
private static Map<String, Map<String, Long>> mergeMapsValue2(List<Map<String, Map<String, Long>>> valueList) {
return valueList.stream()
.flatMap(e -> e.entrySet().stream())
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collector.of(
HashMap::new,
(r, t) -> t.getValue().forEach((k, v) -> r.merge(k, v, Long::sum)),
(r1, r2) -> { r2.forEach((k, v) -> r1.merge(k, v, Long::sum)); return r1; }
)
));
}
As a starting point, converting to use computeIfAbsent and merge gives us the following:
private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) {
final Map<K1, Map<K2, Long>> result = new HashMap<>();
for (final Map<K1, Map<K2, Long>> map : valueList) {
for (final Map.Entry<K1, Map<K2, Long>> sub : map.entrySet()) {
for (final Map.Entry<K2, Long> subsub : sub.getValue().entrySet()) {
result.computeIfAbsent(sub.getKey(), k1 -> new HashMap<>())
.merge(subsub.getKey(), subsub.getValue(), Long::sum);
}
}
}
return result;
}
This removes much of the logic from your inner loop.
This code below is wrong, I leave it here for reference.
Converting to the Stream API is not going to make it neater, but lets give it a go.
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
private static <K1, K2> Map<K1, Map<K2, Long>> mergeMapsValue(List<Map<K1, Map<K2, Long>>> valueList) {
return valueList.stream()
.flatMap(v -> v.entrySet().stream())
.collect(groupingBy(Entry::getKey, collectingAndThen(mapping(Entry::getValue, toList()), l -> l.stream()
.reduce(new HashMap<>(), (l2, r2) -> {
r2.forEach((k, v) -> l2.merge(k, v, Long::sum);
return l2;
}))));
}
This is what I've managed to come up with - it's horrible. The problem is that with the foreach approach, you have a reference to each level of the iteration - this makes the logic simple. With the functional approach, you need to consider each folding operation separately.
How does it work?
We first stream() our List<Map<K1, Map<K2, Long>>>, giving a Stream<Map<K1, Map<K2, Long>>>. Next we flatMap each element, giving a Stream<Entry<K1, Map<K2, Long>>> - so we flatten the first dimension. But we cannot flatten further as we need to K1 value.
So we then use collect(groupingBy) on the K1 value giving us a Map<K1, SOMETHING> - what is something?
Well, first we use a mapping(Entry::getValue, toList()) to give us a Map<K1, List<Map<K2, Long>>>. We then use collectingAndThen to take that List<Map<K2, Long>> and reduce it. Note that this means we produce an intermediate List, which is wasteful - you could get around this by using a custom Collector.
For this we use List.stream().reduce(a, b) where a is the initial value and b is the "fold" operation. a is set to new HashMap<>() and b takes two values: either the initial value or the result of the previous application of the function and the current item in the List. So we, for each item in the List use Map.merge to combine the values.
I would say that this approach is more or less illegible - you won't be able to decipher it in a few hours time, let alone a few days.
I took the flatMap(e -> e.entrySet().stream()) part from Tunaki, but used a shorter variant for the collector:
Map<String, Integer> merged = maps.stream()
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
More elaborate example:
Map<String, Integer> a = new HashMap<String, Integer>() {{
put("a", 2);
put("b", 5);
}};
Map<String, Integer> b = new HashMap<String, Integer>() {{
put("a", 7);
}};
List<Map<String, Integer>> maps = Arrays.asList(a, b);
Map<String, Integer> merged = maps.stream()
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue, Integer::sum));
assert merged.get("a") == 9;
assert merged.get("b") == 5;

Categories

Resources