How to flatten a Map by turning its values into keys - java

How can I transform a Map<String, List<String>> into flattened map based on values grouped as keys.
I.e. from Map<String, List<String>> to Map<String, String> (flattened by values)
For an example.
Source map:
<"fuel", ["BMW", "Honda"]>,
<"electric", ["Tesla", "Nio"]>
Flattened map:
[
"BMW" : "fuel",
"Honda" : "fuel",
"Tesla" : "electric",
"Nio" : "electric"
]

It is not possible to have multiple keys identical according to equals/hashCode within the same map because it contradicts with idea of the map data structure. Every key must be unique, you can't store multiple entries with the same key in a Map.
But you can create a list of Map.Entry objects.
For that, you need to create a stream over the entry set and then flatten each entry by creating a new entry based on car brands (elements of a flatted value) for each key:
Map<String, List<String>> cars =
Map.of("fuel", List.of("BMW", "Honda"),
"electric", List.of("Tesla", "Nio"));
List<Map.Entry<String, String>> entries =
cars.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(brand -> Map.entry(entry.getKey(), brand)))
.collect(Collectors.toList());
System.out.println(entries);
output
[electric=Tesla, electric=Nio, fuel=BMW, fuel=Honda]
Or maybe your intention was to create a Map<String, String> that will allow to retrieve car type based on brand (like that: "BMW" : "fuel", "Honda" : "fuel") then it'll make sense.
The overall approach will be similar to the previous one:
create a stream over the entry set;
flatten each entry using flatMap() by turning an element in the value-list into a new entry;
collect elements to a map with Collectors.toMap().
But there's a caveat: all values have to be unique or there must be a rule on how to combine/discarde car types.
I'll make an assumption that all brands in the map are unique, otherwise, the code below will fail (to deal with collisions Collectors.toMap() requires the third argument - mergeFunction).
Map<String, String> typeByBrand =
cars.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(brand -> Map.entry(brand, entry.getKey())))
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue));
System.out.println(typeByBrand);
output
{Nio=electric, Tesla=electric, BMW=fuel, Honda=fuel}

Related

How to flatten a map inside a list?

I have a list of maps, each map showing the same key attributes type and value.
Question: how can I use java streams to flatten this into a Map<String, List>>?
List<Map<String, Object>> source = List.of(
Map.of("type", "first_type", "value", "1"),
Map.of("type", "first_type", "value", "2"),
Map.of("type", "second_type", "value", "3")
);
Goal:
Map<String, List<Object>> goal = Map.of(
"first_type", List.of("1", "2"),
"second_type", List.of("3")
);
It's probably similar to the following, but how can I group the nested map values by their type?
source.stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
We can achieve this using Collectors.groupingBy.
Map<String, List<Object>> goal = source.stream()
.collect(Collectors.groupingBy(e -> (String) e.get("type"),
Collectors.mapping(e -> e.get("value"), Collectors.toList())));
Since the key of the result map is a String, there is an ugly type-casting when extracting the key (type).
You can use Collectors.groupingBy to grouping by type and Collectors.mapping to map the value and collect as List
Map<Object, List<Object>> goal =
source.stream()
.collect(Collectors.groupingBy(m -> m.get("type"),
Collectors.mapping(m -> m.get("value"), Collectors.toList())));
It's better get result in Map<Object, List<Object>> as map key is Object to avoid type casting.
Just Stream the maps and pull off the type and values. Then map them to a list.
This uses groupingBy to group the values into a target supplier. But you need to map the value first and then specify the supplier as a List. I also changed Object to String since that is what you are using.
Map<String, List<String>> map =
source.stream()
.collect(Collectors.groupingBy(m->m.get("type"),
Collectors.mapping(m->m.get("value"),
Collectors.toList())));
map.entrySet().forEach(System.out::println);
Prints
first_type=[1, 2]
second_type=[3]
Of course, you could also do it like this. This translates to:
if the ArrayList is there for the specified key type, use it.
otherwise create it.
In either case it returns that list from the compute method where
the value for the value key is added.
Map<String,List<String>> map = new HashMap<>();
for(Map<String,String> m : source) {
map.compute(m.get("type"), (k,v)-> v == null ?
new ArrayList<>(): v).add(m.get("value"));
}

Java Stream compare and filter entries from Map<String, Map<String, List<String>>>

I have the following class:
public class SomeObject {
private String id;
private String parentId;
private String type;
//constructor,getters,setters
}
And the following use case:
The field values are not unique. I have a List of SomeObject. First I want to know which SomeOjects share the same parentId and secondly which of those share the same type. First I wanted to group them into the following structure:
Map<String, Map<String, List<String>>>
The key of the first map is the parentId and the value is another map. The key of the second map is the type and the value of the second map is a list of ids from the SomeObjects.
I was able to do this as follows:
Map<String, Map<String, List<String>>> firstTry =
SomeObjects.stream()
.collect(
groupingBy(
SomeObject::getParentId,
groupingBy(
SomeObject::getType,
mapping(SomeObject::getId, toList()))));
And now comes the part where I need some help:
I now want to filter this created map as follows:
Lets assume I have 3 parentId keys which each then have a map with two keys: type1 and type2. (and 2 lists of ids as values)
If the list of ids from type2 contains more/less/different ids than the list of ids from type1, then I want to delete/filter out their parentId entry. And I want to do that for each parentId.
Is there any way with streams to cleanly achieve this?
If you need to retain only those parentId which have the same ids per type, it can be done by converting lists of ids into set and checking the set size:
List<SomeObject> list = Arrays.asList(
new SomeObject("id1", "p1", "type1"),
new SomeObject("id1", "p1", "type2"),
new SomeObject("id2", "p2", "type1"),
new SomeObject("id3", "p2", "type1"),
new SomeObject("id2", "p2", "type2"),
new SomeObject("id4", "p3", "type1"),
new SomeObject("id4", "p3", "type2"),
new SomeObject("id5", "p3", "type2")
);
//.. building firstTry as in the initial code snippet
System.out.println(firstTry);
firstTry.entrySet().stream()
.filter(e -> new HashSet(e.getValue().values()).size() == 1)
.forEach(System.out::println);
Output:
{p1={type2=[id1], type1=[id1]}, p2={type2=[id2], type1=[id2, id3]}, p3={type2=[id4, id5], type1=[id4]}}
p1={type2=[id1], type1=[id1]}
Some points I would use to improve the code before the answere:
I would leave the SomeObject pointer instead of the String literal in the map. Not only they are gonna be more memory efficient most of the time (8 bytes fixed vs 2*character bytes String) but also much more convenient to access the data.
I would also make the type String an enum type.
But getting to the core of the question, i'm sorry.
Lets assume I have 3 parentId keys which each then have a map with two keys: type1 and type2. (and 2 lists of ids as values)
{
'p1': {
'type1':['1','2','uuid-random-whatever'],
'type2':['asdsad']
},
'p2': {
'type1':['1','2'],
'type2':['asdsad','i','want','more','power']
},
'p3': {
'type1':['2'],
'type2':['2']
}
}
Something like this
So for the filtering itself
Map<String, Map<String, List<String>>> theMap
= buildMapExample();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2LessElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
entry.getValue().get("type2").size() < entry.getValue().get("type1").size();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2MoreElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
entry.getValue().get("type2").size() > entry.getValue().get("type1").size();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2SameElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
(new HashSet<>(entry.getValue().get("type2")))
.equals(new HashSet<>(entry.getValue().get("type1")));
theMap = theMap.entrySet().stream()
.filter(
// Choose your lambda for more/less/different. for example
filterType2LessElementsType1
)
.collect(
Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)
);
printMap(theMap);
This code would work. I left out the building/printing to not make this larger as it should. Try it out

Filter on a value of inner Map using Java 8 streams

I have a map of maps - Map> - collection.
I need to filter the map and get the outer map that has a given value for a given key of the inner map.
I tried some combinations, but it is not working.
How do I achieve this.
This is what I have tried
Map<String, Map<String, String>> originalMap = getOriginalMap();
String channelId = "channelIdVal";
Map<String, Map<String, String>> filteredMapForKey = originalMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().entrySet().stream().filter(innerMap -> innerMap.getValue().equalsIgnoreCase(channelId)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
Basically, I am expecting the filteredMapForKey to have a single entry whose inner map ( the value of the entry ) to contain a key whose value is channelId
How do I achieve that. The above code is returning the entire original map with same keys but empty inner maps except for the valid one. For the valid map, instead of returning the complete inner map, it is only return the map with the key and value of matching channel id
Thanks
Seems like there are two things to correct here:
The filter logic to filter in entries instead of filtering them out.
To filter only those outer entries for which inner map satisfies the condition stated.
You can achieve that as:
Map<String, Map<String, String>> filteredMapForKey = originalMap.entrySet()
.stream()
.filter(e -> e.getValue()
.values()
.stream()
.anyMatch(innerMapVal -> innerMapVal.equalsIgnoreCase(channelId)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
This filters all the outer map entries such that, the inner map for them has a value that is equal(ignoreCase) to the given channelId and then collects these entries into a similar Map as the input.

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

Java 8 streams: iterate over Map of Lists

I have the following Object and a Map:
MyObject
String name;
Long priority;
foo bar;
Map<String, List<MyObject>> anotherHashMap;
I want to convert the Map in another Map. The Key of the result map is the key of the input map. The value of the result map ist the Property "name" of My object, ordered by priority.
The ordering and extracting the name is not the problem, but I could not put it into the result map. I do it the old Java 7 way, but it would be nice it is possible to use the streaming API.
Map<String, List<String>> result = new HashMap<>();
for (String identifier : anotherHashMap.keySet()) {
List<String> generatedList = anotherHashMap.get(identifier).stream()...;
teaserPerPage.put(identifier, generatedList);
}
Has anyone an idea? I tried this, but got stuck:
anotherHashMap.entrySet().stream().collect(Collectors.asMap(..., ...));
Map<String, List<String>> result = anotherHashMap
.entrySet().stream() // Stream over entry set
.collect(Collectors.toMap( // Collect final result map
Map.Entry::getKey, // Key mapping is the same
e -> e.getValue().stream() // Stream over list
.sorted(Comparator.comparingLong(MyObject::getPriority)) // Sort by priority
.map(MyObject::getName) // Apply mapping to MyObject
.collect(Collectors.toList())) // Collect mapping into list
);
Essentially, you stream over each entry set and collect it into a new map. To compute the value in the new map, you stream over the List<MyOjbect> from the old map, sort, and apply a mapping and collection function to it. In this case I used MyObject::getName as the mapping and collected the resulting names into a list.
For generating another map, we can have something like following:
HashMap<String, List<String>> result = anotherHashMap.entrySet().stream().collect(Collectors.toMap(elem -> elem.getKey(), elem -> elem.getValue() // can further process it);
Above I am recreating the map again, but you can process the key or the value according to your needs.
Map<String, List<String>> result = anotherHashMap.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream()
.sorted(comparing(MyObject::getPriority))
.map(MyObject::getName)
.collect(Collectors.toList())));
Similar to answer of Mike Kobit, but sorting is applied in the correct place (i.e. value is sorted, not map entries) and more concise static method Comparator.comparing is used to get Comparator for sorting.

Categories