Java 8 groupingby Into map that contains a list - java

I have the following data:
List<Map<String, Object>> products = new ArrayList<>();
Map<String, Object> product1 = new HashMap<>();
product1.put("Id", 1);
product1.put("number", "123");
product1.put("location", "ny");
Map<String, Object> product2 = new HashMap<>();
product2.put("Id", 1);
product2.put("number", "456");
product2.put("location", "ny");
Map<String, Object> product3 = new HashMap<>();
product3.put("Id", 2);
product3.put("number", "789");
product3.put("location", "ny");
products.add(product1);
products.add(product2);
products.add(product3);
I'm trying to stream over the products list, group by the id and for each id have a list on number, while returning a Map that contains three keys: Id, List of number, and a location.
So my output would be:
List<Map<String, Object>>> groupedProducts
map[0]
{id:1, number[123,456], location:ny}
map[1]
{id:2, number[789], location:ny}
I have tried:
Map<String, List<Object>> groupedProducts = products.stream()
.flatMap(m -> m.entrySet().stream())
.collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())));
which prints:
{number=[123, 456, 789], location=[ny, ny, ny], Id=[1, 1, 2]}
I realise Map<String, List<Object>> is incorrect, but it's the best I could achieve to get the stream to work. Any feedback is appreciated.

In your case grouping by Id key with Collectors.collectingAndThen(downstream, finisher) could do the trick. Consider following example:
Collection<Map<String, Object>> finalMaps = products.stream()
.collect(groupingBy(it -> it.get("Id"), Collectors.collectingAndThen(
Collectors.toList(),
maps -> (Map<String, Object>) maps.stream()
.reduce(new HashMap<>(), (result, map) -> {
final List<Object> numbers = (List<Object>) result.getOrDefault("number", new ArrayList<>());
result.put("Id", map.getOrDefault("Id", result.getOrDefault("Id", null)));
result.put("location", map.getOrDefault("location", result.getOrDefault("location", null)));
if (map.containsKey("number")) {
numbers.add(map.get("number"));
}
result.put("number", numbers);
return result;
}))
)
)
.values();
System.out.println(finalMaps);
In the first step you group all maps with the same Id value to a List<Map<String,Object>> (this is what Collectors.toList() passed to .collectingAndThen() does). After creating that list "finisher" function is called - in this case we transform list of maps into a single map using Stream.reduce() operation - we start with an empty HashMap<String,Object> and we iterate over maps, take values from current map in iteration and we set values according to your specification ("Id" and "location" gets overridden, "number" keeps a list of values).
Output
[{number=[123, 456], location=ny, Id=1}, {number=[789], location=ny, Id=2}]
To make code more simple you can extract BiOperator passed to Stream.reduce to a method and use method reference instead. This function defines what does it mean to combine two maps into single one, so it is the core logic of the whole reduction.

Related

Convert a list of maps to a map of lists using Java Streams

I would like to convert a list of maps to a map of lists. It is guaranteed that all the maps have the same keys.
For example, the following list
[{key1: val1, key2: val2}, {key1: val3, key2: val4}]
gets converted to
{key1: [val1, val3], key2: [val2, val4]}
The below code does it using for loops
List<Map<String, Object>> data = getData();
Map<String, List<Object>> result = new HashMap<>();
for (Map<String, Object> element: data) {
element.forEach((key, val) -> {
if (result.containsKey(key))
result.get(key).add(val);
else
result.put(key, Arrays.asList(val));
});
}
How would I be able to achieve the same result using streams?
E.g. like this, relying on the collect stage of java streams.
List<Map<String, Object>> data = new ArrayList<>();
HashMap<String, List<Object>> transposed = data.stream()
.flatMap(i -> i.entrySet().stream())
.collect(Collectors.groupingBy(
Map.Entry::getKey,
HashMap::new,
Collectors.mapping(Map.Entry::getValue,
Collectors.toCollection(ArrayList::new))));
There are a number of such application on SO (e.g. this one), though not precisely tailored to this example.
This version relying on stream.reduce should do it:
// some values for testing
List<Map<String, Object>> list = new ArrayList<>();
list.add(Map.of("1", "One", "2", "Two"));
list.add(Map.of("3", "Three", "4", "Four", "5", "Five"));
list.add(Map.of("3", "Teen", "5", "Penta"));
list.add(Map.of("3", "Trice", "4", "Quattro"));
// conversion with stream reduction
Map<String, List<Object>> result = list.stream().reduce(new HashMap<>(),
(acc, map) -> {
map.forEach((k, v) -> acc.computeIfAbsent(k,
l -> new ArrayList<Object>()).add(v));
return acc;
},
(acc1, acc2) -> {
acc1.putAll(acc2);
return acc1;
});
// output
System.out.println(result);
//{1=[One], 2=[Two], 3=[Three, Teen, Trice], 4=[Four, Quattro], 5=[Five, Penta]}
Explanation: this code goes through each map in the list and accumulates it in the hashmap (1st parameter to the reduce function), with the accumulating function (2nd parameter) adding a new list to the hashmap if it sees a key for the first time (otherwise it accumulates in the list already present); the third function is required for consistency between running the stream function serially or in parallel (it combines partial results in parallel runs).
You could do it without streams, yet in a modern fashion, as follows:
Map<String, List<Object>> result = new HashMap<>();
data.forEach(element ->
element.forEach((k, v) - >
result.computeIfAbsent(k, x -> new ArrayList<>()).add(v)));
This iterates the list and then each map and uses Map.computeIfAbsent to accomodate entries in the result map.
You can use Collectors.toMap method:
List<Map<String, String>> listOfMaps = List.of(
Map.of("key1", "val1", "key2", "val2"),
Map.of("key1", "val3", "key2", "val4"));
Map<String, List<String>> mapOfLists = listOfMaps.stream()
// Stream<Map<String,String>>
.flatMap(map -> map.entrySet().stream())
// Stream<Map.Entry<String,String>>
.collect(Collectors.toMap(
// key
Map.Entry::getKey,
// value
e -> List.of(e.getValue()),
// merge function
(list1, list2) -> {
List<String> list = new ArrayList<>();
list.addAll(list1);
list.addAll(list2);
return list;
}, // map factory
LinkedHashMap::new));
System.out.println(listOfMaps);
// [{key1=val1, key2=val2}, {key1=val3, key2=val4}]
System.out.println(mapOfLists);
// {key1=[val1, val3], key2=[val2, val4]}
See also: Convert a map of lists into a list of maps

Java 8 | Finding Map Entry with highest size of value

I have model Person[city, name]. I have collected them in Map And Grouped them by city. I need to trace the city that has most no of person staying there and return only that entry as part of Map. I'v tried and also it is working but i was wondering is there any better way of doing.
Comparator<Entry<String, List<Person>>> compareByCityPopulation =
Comparator.comparing(Entry<String, List<Person>>::getValue, (s1, s2) -> {
return s1.size() - s2.size();
});
HashMap mapOfMostPopulatedCity = persons.stream()
.collect(Collectors.collectingAndThen(Collectors.groupingBy(Person::getCity), m -> {
Entry<String, List<Person>> found = m.entrySet().stream().max(compareByCityPopulation).get();
HashMap<String, List<Person>> hMap = new HashMap<>();
hMap.put(found.getKey(), found.getValue());
return hMap;
}));
System.out.println("*City with Most no of people*");
mapOfMostPopulatedCity.forEach((place, peopleDetail) -> System.out.println("Places " + place + "-people detail-" + peopleDetail));
Please Suggest how can we write better in java 8.
After getting the max map entry, you have to convert that into a map which has a single entry. For that you can use Collections.singletonMap()
Map<String, List<Person>> mapOfMostPopulatedCity = persons.stream()
.collect(Collectors.groupingBy(Person::getCity)).entrySet().stream()
.max(Comparator.comparingInt(e -> e.getValue().size()))
.map(e -> Collections.singletonMap(e.getKey(), e.getValue()))
.orElseThrow(IllegalArgumentException::new);
With Java9 you can use Map.of(e.getKey(), e.getValue()) to build the map with a single entry.
Suppose if you have an list of persons
List<Person> persons = new ArrayList<Person>();
Then first group By them based on city and then get the Entry with max values in list max will return Optional of Entry, so i won't make it complicate i will just use HashMap to store the result if it present in optional or else will return the empty Map
Map<String, List<Person>> resultMap = new HashMap<>();
persons.stream()
.collect(Collectors.groupingBy(Person::getCity)) //group by city gives Map<String,List<Person>>
.entrySet()
.stream()
.max(Comparator.comparingInt(value->value.getValue().size())) // return the Optional<Entry<String, List<Person>>>
.ifPresent(entry->resultMap.put(entry.getKey(),entry.getValue()));
//finally return resultMap

How to convert Stream<Map<Integer, String>> to map java 8

Here I am posting sample datastructure
I have a list List<Result> resultsList;
class Result {
String name;
Map<String,Integer> resultMap;
}
Now I would like to stream through this list and get the map.
resultList.stream().filter(result->"xxx".equals(result.getName()))
.map(result->result.getResultMap);
It returns Stream<Map<String,Integer>> but I need only Map<String,Integer>.
How to get it using java 8 streams?
Update:
As geneqew mentioned
This is how my datastructure looks
List<Result> resultsList;
Map<String, Integer> map1 = new HashMap<>();
map1.put("m1", 1);
Map<String, Integer> map2 = new HashMap<>();
map2.put("m2", 2);
Map<String, Integer> map3 = new HashMap<>();
map3.put("m3", 3);
results = Arrays.asList(
new Result("r1", map1),
new Result("r2", map2),
new Result("r3", map3)
);
I would like to retrieve single map based on name.
for (Result result: resultsList)
{
if ('xxx'.equals(result.getName())
{
return result.getResultMap();
}
}
Since you want to return the result map of the first Result element to pass your filter, you can obtain it with findFirst():
Optional<Map<String,Integer>> resultMap =
resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(Result::getResultMap)
.findFirst();
You can extract the Map from the Optional this way:
Map<String,Integer> resultMap =
resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(Result::getResultMap)
.findFirst()
.orElse(null);
if you're only looking for one item:
resultList.stream()
.filter(result -> "xxx".equals(result.getName()))
.map(Result::getResultMap)
.findAny();
if the filter could match more than one item then you'll need to flatten then toMap it:
resultList.stream()
.filter(result-> "xxx".equals(result.getName()))
.flatMap(result -> result.getResultMap().entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
if there can be duplicates then use the merge function to resolve collisions:
resultList.stream()
.filter(result -> "xxx".equals(result.getName()))
.flatMap(result -> result.getResultMap().entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (l, r) -> l));
Since you only wanted the map that matches the results' name then:
results.stream()
.filter(r-> r.getName().equals("r2"))
.map(r-> r.getResultMap())
.findFirst()
.orElse(null);
given you have a sample content of:
List<Result> results;
Map<String, Integer> map1 = new HashMap<>();
map1.put("m1", 1);
Map<String, Integer> map2 = new HashMap<>();
map2.put("m2", 2);
Map<String, Integer> map3 = new HashMap<>();
map3.put("m3", 3);
results = Arrays.asList(
new Result("r1", map1),
new Result("r2", map2),
new Result("r3", map3)
);
A bit of explanation, you got a stream because the last operation in your stream is a map; assuming in your list its possible to have more than 1 result with the same name, findFirst will return the first match if found otherwise an empty optional is returned; Finally orElse to get terminate the stream, providing a null value on empty match.
So I want to explain why you receive stream and not a map. The reason of this is because in the beginning you have List with Result objects that you filter by some criteria (in your case "xxx".equals(result.getName())).
Now you can have as result zero, one or more elements that will pass this criteria! Java does not know how many elements will pass at compile time and that is why you get Stream.
Imagine situation that you have two Result objects that have the same name 'xxx' then you will have two maps. The question is what you want to do? If you get only one of the maps you will loose information. If you want to get all of them, please try something like this:
List<Map<String,Integer>> listWithResultMaps = resultList.stream()
.filter(result->"xxx".equals(result.getName()))
.map(result->result.getResultMap())
.collect(Collectors.toList());
Now in this listWithResultMaps you can process all maps that you have as result of your filter.
Good Luck!

Convert java list to map using stream with indexes

I'm trying to learn how to use the Java 8 collections and I was wondering if there was a way to convert my list to a map using a java stream.
List<PrimaryCareDTO> batchList = new ArrayList<>();
PrimaryCareDTO obj = new PrimaryCareDTO();
obj.setProviderId("123");
obj.setLocatorCode("abc");
batchList.add(obj);
obj = new PrimaryCareDTO();
obj.setProviderId("456");
obj.setLocatorCode("def");
batchList.add(obj);
I'm wondering how I would go about creating my list above into a map using a stream. I know how to use the foreach etc with puts, but I was just wondering if there was a more elegant way to build the map using a stream. (I'm aware the syntax below is not correct, I'm new to streams and not sure how to write it)
AtomicInteger index = new AtomicInteger(0);
Map<String, Object> result = batchList.stream()
.map("providerId" + index.getAndIncrement(), PrimaryCareDTO::getProviderId)
.map("locatorCode" + index.get(), PrimaryCareDTO::getLocatorCode);
The goal is to represent the following.
Map<String, Object> map = new HashMap<>();
//Group a
map.put("providerId1", "123");
map.put("locatorCode1", "abc");
//Group b
map.put("providerId2", "456");
map.put("locatorCode2", "def");
...
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Map.Entry;
...
AtomicInteger index = new AtomicInteger(0);
List<SimpleEntry<String, String>> providerIds =
batchList.stream()
.map(e -> new SimpleEntry<>("providerId" + index.incrementAndGet(), e.getProviderId()))
.collect(Collectors.toList());
index.set(0);
List<SimpleEntry<String, String>> locatorCodes =
batchList.stream()
.map(e -> new SimpleEntry<>("locatorCode" + index.incrementAndGet(), e.getLocatorCode()))
.collect(Collectors.toList());
Map<String, String> map = Stream.of(providerIds,
locatorCodes)
.flatMap(e -> e.stream())
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
First it creates two lists, using Entry (from Map) to represent String-String tuples:
list with tuples providerId# as 'key' with the values e.g. "123"
list with tuples locatorCode# as 'key' with the values e.g. "abc"
It then creates a stream containing these two lists as 'elements', which are then concatenated with flatMap() to get a single long stream of Entry,
(The reason the first two can't stay stream and I have to go through a List and back to stream is because the two invocations of index.incrementAndGet() would otherwise only be evaluated when the streams are consumed, which is after index.set(0);.)
It then creates new key-value pairs with the counter and puts them into a map (with Collectors.toMap().
You would have to steam twice as you want to add two of the properties to map
AtomicInteger index = new AtomicInteger(1);
Map<String, String> result1 = batchList.stream()
.collect(Collectors
.toMap(ignored -> "providerId" + index.getAndIncrement(), PrimaryCareDTO::getProviderId)
);
index.set(1);
Map<String, String> result2 = batchList.stream()
.collect(Collectors
.toMap(ignored -> "locatorCode" + index.getAndIncrement(), PrimaryCareDTO::getLocatorCode)
);
Map<String, String> result = new HashMap<>();
result.putAll(result1);
result.putAll(result2);

Java: join two lists of Map<String, Object> using Stream

I have two lists of Map<String, Object> as shown below:
List1=[ {ID=1, actor="A", film="AA"},
{ID=1, actor="B", film="AA"} ]
List2={ [ID = 1, director="C"] }
Result = { [ID=1, actor="A", film="AA", director="C"],
[ID=1, actor="B", film="AA", director="C"] }
I want to use the Stream class in Java 8 to join these lists.
How do I join the to get the value of Result shown?
Is the Stream class in Java 8 fast and stable if List1 and List2 are very big?
Ah now I understand what you want :)
I don't know if there is a better way with streams but here is a solution which would work.
List<Map<String, String>> resultList = l1.stream()
.map(m1 -> {
Map<String, String> map = new HashMap<>();
map.putAll(m1);
l2.stream()
.filter(m2 -> map.get("ID").equals(m2.get("ID")))
.findFirst()
.ifPresent(m2 -> map.put("director", m2.get("director")));
return map;
})
.collect(Collectors.toList());
The above code generates a new List resultList and does not modify the other lists List1 and List2. If it does not matter if List1 gets modified or not you could do it in a cleaner, more readable way.
l1.forEach(m1 -> l2.stream()
.filter(m2 -> m1.get("ID").equals(m2.get("ID")))
.findFirst()
.ifPresent(m2 -> m1.putIfAbsent("director", m2.get("director"))));
This way the entries of list1 get modified. So with the above example list1 is becomes the joined list. But it's actually good practice to have methods without any side effects. So I would not prefer the above example.
I would recommend a method getJoinedList which returns a new List and does not modify the other lists. And in this case I would not use streams but the old-fashioned for-loop.
private static List<Map<String, String>> getJoinedList(
List<Map<String, String>> l1, List<Map<String, String>> l2) {
List<Map<String, String>> result = new ArrayList<>();
for (Map<String, String> m1 : l1) {
Map<String, String> newMap = new HashMap<>();
newMap.putAll(m1);
for (Map<String, String> m2 : l2) {
if (m1.get("ID").equals(m2.get("ID"))) {
newMap.put("director", m2.get("director"));
break;
}
}
result.add(newMap);
}
return result;
}
Then you just can call the method like this.
List<Map<String, String>> joinedList = getJoinedList(l1, l2);
If performance matters, you should first build an index of directors:
Map<Object, Object> directors = list2.stream()
.collect(Collectors.toMap(m -> m.get("ID"), m -> m.get("director")));
Then you can merge the directors to the list entries easily:
list1.stream().forEach(m -> m.put("director", directors.get(m.get("ID"))));
Accesing the director via a Map will be faster than searching the director for each list entry.

Categories

Resources