Convert Java Map<Obj1, Collection<Obj2>> to Map<Obj2, Collection<Obj1>> - java

What would be a good practice to convert Map<Obj1, Collection<Obj2>> to Map<Obj2, Collection<Obj1>>?
I tried doing it with MultiMap.
Map<Obj1, Collection<Obj2>> originalMap = ...;
Multimap<Obj1, Obj2> multiMap = ArrayListMultimap.create();
originalMap.forEach(multiMap::putAll);
Map<Obj2, Collection<Obj1>> convertedMap = Multimaps.invertFrom(multiMap, ArrayListMultimap.create()).asMap();
Is there a better way to do this?

You can achieve the same with Java 8 Streams;
Map<Obj1, List<Obj2>> originalMap = ...
Map<Obj2, List<Obj1>> reversedMap = originalMap.entrySet().stream()
.flatMap(obj2s -> obj2s.getValue().stream()
.map(obj2 -> Map.entry(obj2, obj2s.getKey())))
.collect(
Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
)
);
The fltMap coverts Map<Obj1, List<Obj2>> into a tuple like <Obj2, Obj1>, then collect the result grouping by Obj2.

Your approach is correct, but could be even more straightforward if you used Multimap instead in the first place and
you don't need mutability,
you don't have nulls.
If that's the case, all you need is ImmutableMultimap#inverse():
ImmutableListMultimap<Integer, String> originalMultimap = ImmutableListMultimap.of(
1, "foo",
1, "bar",
2, "baz",
42, "foo"
);
ImmutableListMultimap<String, Integer> convertedMap = originalMultimap.inverse();
System.out.println(convertedMap); // {foo=[1, 42], bar=[1], baz=[2]}
I used ImmutableListMultimap instead of ImmutableMultimap because docs encourage to do so:
Warning: avoid direct usage of ImmutableMultimap as a type (as with Multimap itself). Prefer subtypes such as ImmutableSetMultimap or ImmutableListMultimap, which have well-defined equals(java.lang.Object) semantics, thus avoiding a common source of bugs and confusion.

Related

How to convert a List of strings into a Map with Streams

I have to convert a list List<String> into a map Map<String, Float>.
The keys of the map must be strings from the list, and every value must be 0 (zero) for all keys.
How can I convert this?
I've tried:
List<String> list = List.of("banana", "apple", "tree", "car");
Map<String, Float> map = list.stream()
.distinct()
.collect(Collectors.toMap(String::toString, 0f));
If you need to associate every key with 0, the second argument of the toMap() needs to be a function returning 0, but not the value of 0:
Map<String, Float> map = list.stream()
.collect(Collectors.toMap(
Function.identity(), // extracting a key
str -> 0f, // extracting a value
(left, right) -> 0f // resolving duplicates
));
Also, there's no need to use distinct(). Under the hood, it'll create a LinkedHashSet to eliminate duplicates, in case if most of the stream elements are unique and the data set is massive, it might significantly increase memory consumption.
Instead, we can deal with them within the map. For that we need to add the third argument mergeFunction.
And there's no need to invoke toString() method on a stream element which is already a string to extract a key of type String. In such cases, we can use Function.identity() to signify that no transformations required.
Another way doing the the same without using stream.
Map<String,Float> map = new HashMap<String, Float>();
list.forEach(s-> map.computeIfAbsent(s,v->0f));
The below works fine,
Map<String, Float> collect =
list.stream() .collect(Collectors
.toMap(Function.identity(),
a -> 0f));

How to apply IF logic during stream pipeline calculation?

I have a code like this. But it looks a bit ugly
Set<String> strings= new HashSet(Arrays.asList("str1", "str2", "str3" ));
Optional.of(strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(Function.identity(), myMap::get)))
.map(stringListMap -> stringListMap.isEmpty() ? null : stringListMap)
.orElse(myMap)
.entrySet()
.stream()
.flatMap(...)
...
Is there way to avoid collection in the middle of pipeline ?
I think the more imperative solution is actually more readable here.
Map<String, Object> actualMap = myMap; // equiv. to your .orElse(myMap)
if (myMap.keySet().stream().anyMatch(strings::contains)) {
actualMap = strings.stream().collect(Collectors.toMap(Function.identity(), myMap::get));
}
actualMap.entrySet().stream()
.flatMap(...)
Java's a nice language because it allows you to be pragmatic about when you apply functional ideas. Don't feel like you have to use them all the time.
I'm not adding much here - very similar to Michael's answer. But this avoids creating streams out of both myMap and strings.
Map<String, Object> mappedValues = strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(Function.identity(), myMap::get)));
(mappedValues.isEmpty()
? myMap
: mappedValues).entrySet()
.stream()
.flatMap(...)
It's a sort of a late answer, but the following solution does not use Optional and does not create a mapping if no key from strings exists in myMap at the cost of duplicate running the stream on the smaller strings set:
(strings.stream().anyMatch(myMap::containsKey)
? strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(k -> k, myMap::get))
: myMap
)
.entrySet().stream()
.flatMap()

From "Map<String, Collection<String>>" to "Map<String, List<String>>" using Java 8

Is there a better way to transform "Map<String, Collection<String>>" to "Map<String, List<String>>"?
Map<String, Collection<String>> collectionsMap = ...
Map<String, List<String>> listsaps =
collectionsMap.entrySet().stream()
.collect(Collectors.<Map.Entry<String, Collection<String>>,
String, List<String>>toMap(
Map.Entry::getKey,
e -> e. getValue().stream().collect(Collectors.toList())
)
);
Thank you for helping us improve
For cases like this, I'd consider using Map.forEach to perform the operation using side effects. Streams over maps are somewhat cumbersome, as one needs to write extra code to stream the map entries and then extract the key and value from each entry. By contrast, Map.forEach passes each key and value to the function as a separate parameter. Here's what that looks like:
Map<String, Collection<String>> collectionsMap = ...
Map<String, List<String>> listsaps = new HashMap<>(); // pre-size if desired
collectionsMap.forEach((k, v) -> listsaps.put(k, new ArrayList<>(v)));
If your map is large, you'll probably want to pre-size the destination in order to avoid rehashing during its population. To do this properly you have to know that HashMap takes the number of buckets, not the number of elements, as its parameter. This requires dividing by the default load factor of 0.75 in order to pre-size properly given a certain number of elements:
Map<String, List<String>> listsaps = new HashMap<>((int)(collectionsMap.size() / 0.75 + 1));
1) In Collectors.toMap() you don't need to repeat the generic types as these are inferred.
So :
collect(Collectors.<Map.Entry<String, Collection<String>>,
String, List<String>>toMap(...)
can be replaced by :
collect(Collectors.toMap(...)
2) The way of transforming the collection into a List could also be simplified.
This :
e -> e. getValue().stream().collect(Collectors.toList())
could be written as :
e -> new ArrayList<>(e.getValue())
You could write :
Map<String, List<String>> listsaps =
collectionsMap.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new ArrayList<>(e.getValue())
)
);
I think that this is easier to read:
Map<String, List<String>> listsaps = new HashMap<>();
collectionsMap.entrySet()
.stream()
.forEach(e -> listsaps.put(e.getKey(), new ArrayList<>(e.getValue())));
If you just want to convert the entries to lists but don't really care about changing the type of the collection then you can use map.replaceAll:
collectionsMap.replaceAll((k, v) -> new ArrayList<>(v));

Java 8 stream.collect( ... groupingBy ( ... mapping( ... reducing ))) reducing BinaryOperator-usage

I played around with a solution using groupingBy, mapping and reducing
to the following question: Elegantly create map with object fields as key/value from object stream in Java 8. Summarized the goal was to get a map with age as key and the hobbies of a person as a Set.
One of the solutions I came up with (not nice, but that's not the point) had a strange behaviour.
With the following list as input:
List<Person> personList = Arrays.asList(
new Person(/* name */ "A", /* age */ 23, /* hobbies */ asList("a")),
new Person("BC", 24, asList("b", "c")),
new Person("D", 23, asList("d")),
new Person("E", 23, asList("e"))
);
and the following solution:
Collector<List<String>, ?, Set<String>> listToSetReducer = Collectors.reducing(new HashSet<>(), HashSet::new, (strings, strings2) -> {
strings.addAll(strings2);
return strings;
});
Map<Integer, Set<String>> map = personList.stream()
.collect(Collectors.groupingBy(o -> o.age,
Collectors.mapping(o -> o.hobbies, listToSetReducer)));
System.out.println("map = " + map);
I got:
map = {23=[a, b, c, d, e], 24=[a, b, c, d, e]}
clearly not what I was expecting. I rather expected this:
map = {23=[a, d, e], 24=[b, c]}
Now if I just replace the order of (strings, strings2) of the binary operator (of the reducing collector) to (strings2, strings) I get the expected result. So what did I miss here?
Did I misinterpret the reducing-collector? Or which documentation piece did I miss that makes it obvious that my usage was not working as expected?
Java version is 1.8.0_121 if that matters.
Reduction should never modify the incoming objects. In your case, you are modifying the incoming HashSet that is supposed to be the identity value and return it, so all groups will have the same HashSet instance as result, containing all values.
What you need is a Mutable Reduction, which can be implemented via Collector.of(…) like it has been already implemented with the prebuilt collectors Collectors.toList(), Collectors.toSet(), etc.
Map<Integer, Set<String>> map = personList.stream()
.collect(Collectors.groupingBy(o -> o.age,
Collector.of(HashSet::new, (s,p) -> s.addAll(p.hobbies), (s1,s2) -> {
s1.addAll(s2);
return s1;
})));
The reason, we need a custom collector at all, is that Java 8 doesn’t have the flatMapping collector, which Java 9 is going to introduce. With that, the solution will look like:
Map<Integer, Set<String>> map = personList.stream()
.collect(Collectors.groupingBy(o -> o.age,
Collectors.flatMapping(p -> p.hobbies.stream(), Collectors.toSet())));

Flatten a Map<Integer, List<String>> to Map<String, Integer> with stream and lambda

I would like to flatten a Map which associates an Integer key to a list of String, without losing the key mapping.
I am curious as though it is possible and useful to do so with stream and lambda.
We start with something like this:
Map<Integer, List<String>> mapFrom = new HashMap<>();
Let's assume that mapFrom is populated somewhere, and looks like:
1: a,b,c
2: d,e,f
etc.
Let's also assume that the values in the lists are unique.
Now, I want to "unfold" it to get a second map like:
a: 1
b: 1
c: 1
d: 2
e: 2
f: 2
etc.
I could do it like this (or very similarly, using foreach):
Map<String, Integer> mapTo = new HashMap<>();
for (Map.Entry<Integer, List<String>> entry: mapFrom.entrySet()) {
for (String s: entry.getValue()) {
mapTo.put(s, entry.getKey());
}
}
Now let's assume that I want to use lambda instead of nested for loops. I would probably do something like this:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().map(e -> {
e.getValue().stream().?
// Here I can iterate on each List,
// but my best try would only give me a flat map for each key,
// that I wouldn't know how to flatten.
}).collect(Collectors.toMap(/*A String value*/,/*An Integer key*/))
I also gave a try to flatMap, but I don't think that it is the right way to go, because although it helps me get rid of the dimensionality issue, I lose the key in the process.
In a nutshell, my two questions are :
Is it possible to use streams and lambda to achieve this?
Is is useful (performance, readability) to do so?
You need to use flatMap to flatten the values into a new stream, but since you still need the original keys for collecting into a Map, you have to map to a temporary object holding key and value, e.g.
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(e->e.getValue().stream()
.map(v->new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
The Map.Entry is a stand-in for the nonexistent tuple type, any other type capable of holding two objects of different type is sufficient.
An alternative not requiring these temporary objects, is a custom collector:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().collect(
HashMap::new, (m,e)->e.getValue().forEach(v->m.put(v, e.getKey())), Map::putAll);
This differs from toMap in overwriting duplicate keys silently, whereas toMap without a merger function will throw an exception, if there is a duplicate key. Basically, this custom collector is a parallel capable variant of
Map<String, Integer> mapTo = new HashMap<>();
mapFrom.forEach((k, l) -> l.forEach(v -> mapTo.put(v, k)));
But note that this task wouldn’t benefit from parallel processing, even with a very large input map. Only if there were additional computational intense task within the stream pipeline that could benefit from SMP, there was a chance of getting a benefit from parallel streams. So perhaps, the concise, sequential Collection API solution is preferable.
You should use flatMap as follows:
entrySet.stream()
.flatMap(e -> e.getValue().stream()
.map(s -> new SimpleImmutableEntry(e.getKey(), s)));
SimpleImmutableEntry is a nested class in AbstractMap.
Hope this would do it in simplest way. :))
mapFrom.forEach((key, values) -> values.forEach(value -> mapTo.put(value, key)));
This should work. Please notice that you lost some keys from List.
Map<Integer, List<String>> mapFrom = new HashMap<>();
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(integerListEntry -> integerListEntry.getValue()
.stream()
.map(listItem -> new AbstractMap.SimpleEntry<>(listItem, integerListEntry.getKey())))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
Same as the previous answers with Java 9:
Map<String, Integer> mapTo = mapFrom.entrySet()
.stream()
.flatMap(entry -> entry.getValue()
.stream()
.map(s -> Map.entry(s, entry.getKey())))
.collect(toMap(Entry::getKey, Entry::getValue));

Categories