Java Streams in Map - java

Map<Integer,String> maper = new HashMap<>();
maper.put(1, "Naveen");
Map<Integer,Map<Integer,String>> map1 = new HashMap<>();
map1.put(1, maper);
Map<Integer,Map<Integer,Map<Integer,String>>> mapOne = new HashMap<>();
mapOne.put(1, map1);
How to get the String value from mapOne using streams in Java 8?

The simple answer to your question would just be:
mapOne.get(1).get(1).get(1)
where each get gets the inner map, or in the case of the last get, gets the final value.
Since you mentioned streams, I think you probably meant to ask how to flatten the nested map so that you can get the strings in the innermost map as a collection. In that case, you should use flatMap. N calls to flatMap is required for a map nested N levels deep.
mapOne.values().stream()
.flatMap(x -> x.values().stream())
.flatMap(x -> x.values().stream())
.collect(Collectors.toList()) // or toSet()

Related

Grouping of inner Maps with Java Streams

i have following structure:
Map<String,Map<String,Map<String,Integer>>>
Now i want to disregard the First-level-Maps and group (and sum up) the 3rd-Level-Maps according to the key of the 2nd-Level-Maps.
To Clarify some example-Entries:
Entry 1: ["1"["A"[[a,1];[b,2]];"B"[[a,3];[c,1]]]]
Entry 2: ["2"["A"[[b,2];[c,1]];"B"[[a,5];[b,0]]]]
Desired output:
Entry 1: ["A"[[a,1];[b,4];[c,1]]]
Entry 4: ["B"[[a,8];[b,0];[c,1]]]
So to do this I first group my Entry-stream according to my 2nd-Level-Keys ("A","B") and, if nothing else done, end up with a structure like the following:
Map<String,List<Entry<String,Map<String,Integer>>>>
And here is where I am stuck. How do i go about getting my Map<String,Integer>from my List of Entries (for each outer Map, secifically)?
The simple code which I assume is guaranteed to be needed:
initialMap.values().stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey));
Summary:
How do I transform a Map<String,Map<String,Map<String,Integer>>> to a Map<String<Map<String,Integer>>, disregarding the outermost Map, grouping my innermost Maps according to my 2nd-Layer-Keys and summing my Integer-values by key-values of the Innermost Map.
Additionally the outermost Maps each have a Key-Value-Pair for each 2nd-Level-Map, so each will have the same 2nd-Level-Keys. In the 3rd-Level-Keysets can be Keys not found in other 3rd-Level-Maps
Map<String, Map<String, Integer>> result =
initialMap
.values()
.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.groupingBy(e -> mapToFirstEntry(e.getValue()).getKey(),
Collectors.summingInt(e -> mapToFirstEntry(e.getValue()).getValue()))));
but it assumes that a third-level Map<String, Integer> contains one entry and there is a method to get that entry:
public static Map.Entry<String, Integer> mapToFirstEntry(Map<String, Integer> map) {
return map.entrySet().iterator().next();
}
If you have the liberty of using Java9, I would suggest you to use the flatMapping collector to solve this problem. This approach is much more readable and generates less visual clutter to me. Here's how it looks.
Map<String, Map<String, Integer>> summaryMap = map.values().stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.flatMapping(e -> e.getValue().entrySet().stream(),
Collectors.groupingBy(Map.Entry::getKey,
Collectors.summingInt(Map.Entry::getValue)))));
This program produces the following output:
{A={a=1, b=4, c=1}, B={a=8, b=0, c=1}}
A thing to keep in mind here: streams conceptually represent a single element coming down through a "pipe" of sorts. It's always single element when the stream runs, no matter if source has one, multiple or infinite number of elements backed up in total.
What you're trying to do here is represent several nested loops, along the lines of:
Map<String, Map<String, Integer>> result = new HashMap<>();
for (Map<String, Map<String, Integer>> firstMap : inputMap.values()) {
for (Entry<String, Map<String, Integer>> firstEntry : firstMap.entrySet()) {
String upperCaseKey = firstEntry.getKey();
Map<String, Ingeter> resultEntry = result.computeIfAbsent(
upperCaseKey,
_k -> new HashMap<>());
for (Entry<String, Integer> secondEntry : firstEntry.getValue().entrySet()) {
resultEntry.merge(secondEntry.getKey(), secondEntry.getValue(), Integer::sum);
}
}
}
Among the better ways to do it with streams would be via Collector composition:
inputMap.values().stream()
.flatMap(map -> map.entrySet().stream())
.flatMap(firstEntry -> firstEntry.getValue()
.entrySet().stream()
.map(secondEntry -> new SimpleImmutableEntry(
firstEntry.getKey(),
secondEntry)
)
)
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.groupingBy(
compositeEntry -> compositeEntry.getValue().getKey(),
Collectors.summingInt(compositeEntry -> compositeEntry.getValue().getValue())
)
));
That should do the trick in general, but note how I had to first build up a composite entry, to keep element count as 1, and then nested two grouping collectors. This is why I'm of the opinion that tasks like yours aren't good fit for the API. It's also very likely to require a little help from you to compiler, as it may struggle to infer all the types correctly.
Also note, that this is not the only way to do it: the Stream API is very flexible, and you're likely to see many more other ways to do the same.

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

how to avoid a nested loop

I am trying to achieve the following
public class MyObject {
Map<String, String> myMap;
}
public class MyOtherObject {
List<MyObject> myObjects;
}
I want to be able to do the following
for (MyObject myObject: myObjects) {
Map<String,String> newMap = new Hashmap<String,String>();
for (Map.Entry<String, String> entry : myObject.getMyMap.entrySet() {
newMap.put(entry.key + "a" , entry.value)
}
}
How do I avoid this nested loop?
Regarding your current solution, you'll need to move the newMap
declaration outside the loop otherwise you're creating a new map at each iteration of the loop and that would not contain the result you'd expect.
You also have a typo on your map instantiation, change Hashmap to
HashMap.
As for avoiding nested loops, you can create a stream instance of the entrySet then simply perform a reduction operation on the stream using collect.
Map<String, String> copy = myObject.getMyMap.entrySet()
.stream()
.collect(Collectors.toMap(p -> p.getKey() + "a", Map.Entry::getValue));
if you want to copy all of the mappings from the specified map to another map then as svasa has suggested within the comments, you can do:
newMap.putAll(myObject.getMyMap.entrySet().stream().collect(Collectors.toMap(p -> p.getKey() + "a", Map.Entry::getValue)));
Use putAll:
Amap.putAll(Bmap);
The complexity will always be O(n x m), with n being the size of the list and m the maximum size of the inner maps.
With java 8, you can use:
Map<String, String> result = myObjects.stream()
.flatMap(myObject -> myObject.getMyMap().entrySet().stream())
.collect(Collectors.toMap(e -> e.getKey() + "a", Entry::getValue));
This assumes you want only one map with all the entries of all the inner maps, with their key modified.
I've used flatMap to flatten all the streams with the entries of the inner maps into one stream. Then, I've used Collectors.toMap to collect all these entries into a new map.
This will fail if there are repeated keys. In such case, you can use the overloaded version of Collectors.toMap, which accepts a function to merge values when there's a collision between keys:
Map<String, String> result = myObjects.stream()
.flatMap(myObject -> myObject.getMyMap().entrySet().stream())
.collect(Collectors.toMap(e -> e.getKey() + "a", Entry::getValue, String::concat));
This simply concatenates the values that are mapped more than once to the same key, but you can use any other merge function.

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

How to use Map filter in list by Java 8 lambda

Map is Map<String, List<User>> and List is List<User>. I want to use
Map<String,List<User>> newMap = oldMap.stream()
.filter(u ->userList.stream()
.filter(ul ->ul.getName().equalsIgnoreCase(u.getKey()).count()>0))
.collect(Collectors.toMap(u.getKey, u.getVaule()));
can't change to new Map. Why?
There are several problems with your code:
Map does not have a stream(): its entry set does, so you need to call entrySet() first.
There are a couple of misplaced parentheses
Collectors.toMap code is incorrect: you need to use the lambda u -> u.getKey() (or the method-reference Map.Entry::getKey) and not just the expression u.getKey(). Also, you mispelled getValue().
This would be a corrected code:
Map<String, List<User>> newMap =
oldMap.entrySet()
.stream()
.filter(u -> userList.stream()
.filter(ul ->ul.getName().equalsIgnoreCase(u.getKey())).count() > 0
).collect(Collectors.toMap(u -> u.getKey(), u -> u.getValue()));
But a couple of notes here:
You are filtering only to see if the count is greater than 0: instead you could just use anyMatch(predicate). This is a short-cuiting terminal operation that returns true if the predicate is true for at least one of the elements in the Stream. This has also the advantage that this operation might not process all the elements in the Stream (when filtering does)
It is inefficient since you are traversing the userList every time you need to filter a Stream element. It would be better to use a Set which has O(1) lookup (so first you would convert your userList into a userSet, transforming the username in lower-case, and then you would search this set for a lower-case value username).
This would be a more performant code:
Set<String> userNameSet = userList.stream().map(u -> u.getName().toLowerCase(Locale.ROOT)).collect(toSet());
Map<String,List<User>> newMap =
oldMap.entrySet()
.stream()
.filter(u -> userNameSet.contains(u.getKey().toLowerCase(Locale.ROOT)))
.collect(Collectors.toMap(u -> u.getKey(), u -> u.getValue()));
Perhaps you intended to create a Stream of the entry Set of the input Map.
Map<String,List<User>> newMap =
oldMap.entrySet().stream()
.filter(u ->userList.stream().filter(ul ->ul.getName().equalsIgnoreCase(u.getKey())).count()>0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
This would create a Map that retains the entries of the original Map whose keys equal the name of at least one of the members of userList (ignoring case).

Categories