How to flatten a map inside a list? - java

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

Related

How to filter nested Map<String, Object> in Java?

I have Map<String, Object> currencyRate that contains
"license": "https:/etc",
"timestamp": 1654,
"base": "USD",
"rates": {
"AED": 3.6,
"AFN": 88.9,
"ALL": 112,
etc
}
And e.g. I want to get "AED" value which is 3.6.
I've tried this:
Map<String, Object> filtered = currencyRate.entrySet()
.stream()
.filter(map->map.getValue().toString().contains("AED"))
.collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue()));
But it returned this:
"rates": {
"AED": 3.6,
"AFN": 88.9,
"ALL": 112,
etc
}
So "rates" is key, and it has values which also has keys and values.
How can I get keys and values in "rates"?
You don't need to use streams for that. Instead, you need to get the nested map by its key "rates" and type cast it into the appropriate type (note that this casting is unsafe):
double aedRate = ((Map<String, Double>) map.get("rates")).get("AED");
If you want to utilize streams for that at all costs, then again firstly you need to extract the nested map and convert it from the Object type into Map. Then create a stream over its entries and filter out the entry with the target key "AED":
Map.Entry<String, Double> result =
((Map<String, Double>) map.get("rates")).entrySet().stream()
.filter(entry -> entry.getKey("AED"))
.findFirst()
.orElseThrow();
Note that using Object as generic type and operating via type casts isn't a good practice. The overall approach of a Map<String, Object> is error-prone and unmaintainable. You would never had this problem if you kept the data in your application structured appropriately.
Since you're using a map where each value can be any Object Map<String, Object> (there is no declaration so I'm just assuming here), I guess you could use the findFirst terminal operation and then cast the returned value to Map<String, Object> (I've used Object here too for the values since the data type is not specified in the question).
Of course, this makes sense only if you specifically want to use streams and under the strict assumption that there is only one value containing "AED" within your enclosing map (again, in the question is not specified if any other value can contain "AED"). Alternatively, you could simply perform a map.get("rates") to retrieve your nested map.
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>(Map.of(
"license", "http:/etc",
"timestamp", 1654,
"base", "USD",
"rates", Map.of(
"AED", 3.6,
"AFN", 88.9,
"ALL", 112
// ...
)
));
Map<String, Object> mapRes = (Map<String, Object>) map.entrySet()
.stream()
.filter(entry -> entry.getValue().toString().contains("AED"))
.map(entry -> entry.getValue())
.findFirst()
.orElse(null);
System.out.println(mapRes);
}
Output
{ALL=112, AED=3.6, AFN=88.9}
I guess You should use flatMap() to transform nested map to new stream.
Map<String, Object> filtered = currencyRate.entrySet()
.stream()
.filter(map->map.getValue().toString().contains("AED"))
.flatMap(map->map.stream())
.filter(...)
.collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue()));

How do I generate a Map of Map of List (Map<String, Map<Enum, List<String>>>) in java using streams

Given:
enum Food{
FRUITS, VEGGIES;
}
Map<String, List<String>> basketMap = new HashMap<>();
basketMap .put("bucket1", Arrays.asList("apple", "banana"));
basketMap .put("bucket2", Arrays.asList("orange", "kiwi"));
basketMap .put("bucket3", Arrays.asList("banana", "orange"));
Need to generate a Map of map of list(populte fruitBaskerMap)
Map<String, Map<Food, List<String>> fruitBasketMap = new HashMap<>();
Final output:
fruitBasketMap:
[
bucket1, [Food.FRUITS, {"apple", "banana"}],
bucket2, [Food.FRUITS, {"orange", "kiwi"}],
bucket3, [Food.FRUITS, {"banana", "orange"}]
]
I tried the below (but was not successful)
fruitBasketMap = basketMap.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey,
Collectors.toMap(Food.FRUITS,
Collectors.toList(Map.Entry::getValue())
)
)
);
Can somebody let me know how do I do that?
This implementation seems to be working (using Java 9 Map.of):
Map<String, Map<Food, List<String>>> fruitBasketMap = basketMap.entrySet().stream()
.map(e -> Map.entry(e.getKey(), Map.of(Food.FRUITS, e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Output of the test:
{
bucket2={FRUITS=[orange, kiwi]},
bucket3={FRUITS=[banana, orange]},
bucket1={FRUITS=[apple, banana]}
}
This should do the trick in Java 8+ way:
fruitBasketMap =
basketMap.entrySet()
.stream()
.collect(groupingBy(Entry::getKey,
toMap(e -> Food.FRUITS, Entry::getValue)));
The collector above works in the following way:
Groups the elements by their keys, thus the keys remain the same as in the original map.
Collects the entries into a new map with
key: Food.FRUITS (constant for all entries)
value: the original value (list of strings)
This would work with Java 8
Map<String, Map<Food, List<String>>> collect = basketMap.entrySet().stream().collect(
Collectors.toMap(basketMapEntry -> basketMapEntry.getKey(),
basketMapEntry -> basketMapEntry.getValue().stream()
.collect(Collectors.groupingBy(
item -> type.get(item)
)
)
)
);
I am using the groupingBy collector to group the list of items based on the enum.
Assumption is that there would a Map to know the value of Food based on the entry. Something like:
Map<String, Food> type = new HashMap<>();
type.put("apple", Food.FRUITS);
type.put("banana", Food.FRUITS);
type.put("orange", Food.FRUITS);
type.put("kiwi", Food.FRUITS);
type.put("kiwi", Food.FRUITS);
type.put("potato", Food.VEGGIES);
type.put("carrot", Food.VEGGIES);
You can achieve this in Java-8 way itself. Try the below one. Since the inner map will always have single entry, you can use singletonMap
Map<String, Map<Food, List<String>>> result = basketMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> Collections.singletonMap(Food.FRUITS, entry.getValue())));

Inverse Map where getValue returns a List

I would like to transform a Map<String, List<Object>> so it becomes Map<String, String>. If it were just Map<String, Object> it is easy in Java8;
stream().collect(k -> k.getValue().getMyKey(), Entry::getKey);
But this will not work because getValue returns a List Map<List<Object>, String> in my example. Assume Object contains a getter to be used for the key and that Object does not contain the key in the first map.
Any thoughts?
Stream over the list of objects and extract the key you need then map --> flatten --> toMap
source.entrySet()
.stream()
.flatMap(e -> e.getValue()
.stream()
.map(x -> new SimpleEntry<>(x.getMyKey(), e.getKey())))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
use a merge function if there is expected to be duplicate getMyKey() values:
source.entrySet()
.stream()
.flatMap(e -> e.getValue()
.stream()
.map(x -> new SimpleEntry<>(x.getMyKey(), e.getKey())))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue, (l, r) -> l));
Note: the above uses the source map keys as the values of the resulting map as that's what you seem to be illustrating in your post, if however you want the key of the source map to remain as the key of the resulting map then change new SimpleEntry<>(x.getMyKey(), e.getKey()) to new SimpleEntry<>(e.getKey(),x.getMyKey()).
If preference could be to choose any amongst the multiple values mapped as a key, you can simply use:
Map<String, List<YourObject>> existing = new HashMap<>();
Map<String, String> output = new HashMap<>();
existing.forEach((k, v) -> v.forEach(v1 -> output.put(v1.getMyKey(), k)));
Essentially this would put the 'first' such myKey along with its corresponding value which was the key of the existing map.

convert a Map<String, List<MyObject> to List<Map<String, MyObject>> in java

I want to convert a Map<String, List<MyObject>> to List<Map<String, MyObject>>
{<key1,[myObject1, myObject2]>, <key2,[myObject3, myObject4]>}
will be converted to
[{<key1,myObject1>, <key2,myObject3>}, {<key1,myObject2>, <key2, myObject4>}]
where myObject1 and myObject3 have a same unique id and so do myObject2 and myObject4.
my implementation is below but is there a more optimal way of doing this.
private List<Map<String, MyObject>> getObjectMapList( Map<String, List<MyObject>> objectMap)
{
List<Map<String, MyObject>> objectMapList = new ArrayList<Map<String,MyObject>>();
for(MyObject myObject : objectMap.get("key1")) {// there will be default key1 whose value is known
Map<String, MyObject> newMap= new HashMap<String, MyObject>();
for (String key : objectMap.keySet()) {
newMap.put(key, objectMap.get(key).stream()
.filter(thisObject -> thisObject.getId().equals(myObject.getId()))
.collect(Collectors.toList()).get(0));
}
objectMapList.add(newMap);
}
return objectMapList;
}
Here's a 1-liner without any curly brackets:
private List<Map<String, MyObject>> getObjectMapList( Map<String, List<MyObject>> objectMap) {
return map.entrySet().stream()
.map(e -> e.getValue().stream()
.map(o -> Collections.singletonMap(e.getKey(), o))
.collect(Collections.toList())
.flatMap(List::stream)
.collect(Collections.toList());
}
The main "trick" here is the use of Collections.singletonMap() to allow a blockless in-line create-and-populate of a map.
Disclaimer: Code may not compile or work as it was thumbed in on my phone (but there's a reasonable chance it will work)
This stream should return you the desired result. With my old Eclipse version, I had some trouble with types. You might have to break it up into single steps, or add some types in the lambdas, but I wanted to keep it short.
Map<String, List<MyObject>> objectMap = new HashMap<>();
objectMap.keySet()
.stream()
.flatMap(key -> objectMap.get(key)
.stream()
.map(obj -> new AbstractMap.SimpleEntry<>(key, obj)))
.collect(groupingBy(pair -> pair.getValue().getId()))
.values()
.stream()
.map(listOfSameIds -> listOfSameIds.stream()
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
.collect(toList());
What I do is:
Pair all the objects in all your input's values with their keys and put them in one long list (flatMap(key -> streamOfKeyObjectPairs)).
Separate those pairs by the IDs of the objects (collect(groupingBy)).
Take each of those groups and convert the lists of pairs into maps (map(list -> toMap))
Put all those maps into a list

Joining a List<String> inside a map

I'm trying to convert a Map<String, List<String>> to a Map<String, String>, where the value for each key is the joint string built by joining all the values in the List in the previous map, e.g.:
A -> ["foo", "bar", "baz"]
B -> ["one", "two", "three"]
should be converted to
A -> "foo|bar|baz"
B -> "one|two|three"
What's the idiomatic way to do this using the Java 8 Streams API?
Simply use String.join, no need to create the nested stream:
Map<String, String> result = map.entrySet()
.stream()
.collect(toMap(
e -> e.getKey(),
e -> String.join("|", e.getValue())));
You can use Collectors.joining(delimiter) for this task.
Map<String, String> result = map.entrySet()
.stream()
.collect(toMap(
Map.Entry::getKey,
e -> e.getValue().stream().collect(joining("|")))
);
In this code, each entry in the map is collected to a new map where:
the key stays the same
the value, which is a list, is collected to a String by joining all the elements together
Google Guava has a nice helper method for this:
com.google.common.collect.Maps.transformValues(map, x -> x.stream().collect(joining("|")));
using pure java, this would work:
map.entrySet().stream().collect(toMap(Entry::getKey, e -> e.getValue().stream().collect(joining("|"))));

Categories