Java 8 streaming on Map - java

I have a map of map on which I want to stream,
Map<LocalDate, Map<Integer, List<String>>> mainMap;
and remove the values which match another map,
Map<LocalDate, List<String>> childMap;
Ex. mainMap contains
{2016-02-23={120=[1,2,3], 121=[1,2,3], 122=[1,2,3], 123=[1,2,3]}}
and childMap contains
{2016-02-23=[120,123]}
I want to remove these values from mainMap.
Expected Output =>
{2016-02-23={121=[1,2,3], 122=[1,2,3]}}
How to do it in Java 8?

You could do it in-place with the following:
mainMap.forEach((k, v) ->
v.keySet().removeIf(s ->
Optional.ofNullable(childMap.get(k)).map(o -> o.contains(s.toString())).orElse(false)
)
);
This iterates through the mainMap. Then, concerning the value of that map, which is a Map<Integer, List<String>>, it removes all the integer keys (converted to a String) where the childMap for the current date points to a list containing that key. Note that the integer key is not removed if the child map does not contain a list for the current date.
A full sample code is the following, which prints your desired output:
public static void main(String[] args) {
Map<LocalDate, Map<Integer, List<String>>> mainMap = new HashMap<>();
Map<Integer, List<String>> map = new HashMap<>();
map.put(120, Arrays.asList("1", "2", "3"));
map.put(121, Arrays.asList("1", "2", "3"));
map.put(122, Arrays.asList("1", "2", "3"));
map.put(123, Arrays.asList("1", "2", "3"));
mainMap.put(LocalDate.of(2016, 2, 23), map);
Map<LocalDate, List<String>> childMap = new HashMap<>();
childMap.put(LocalDate.of(2016, 2, 23), Arrays.asList("120", "123"));
mainMap.forEach((k, v) ->
v.keySet().removeIf(s ->
Optional.ofNullable(childMap.get(k)).map(o -> o.contains(s.toString())).orElse(false)
)
);
System.out.println(mainMap);
}

An alternative to Tunaki’s answer is
mainMap.forEach((k, v) -> childMap.getOrDefault(k, emptyList())
.stream().map(Integer::valueOf).forEach(v::remove));
the difference is that Tunaki’s solution will iterate over the contents of the sub-maps contained in mainMap and perform a lookup on childMap for each, whereas the solution above will iterate over the lists found in childMap and perform a lookup the submaps of mainMap. This is preferable when the sizes of both, the submaps and the lists, grow.
One thing that hasn’t been addressed, is whether mappings of the mainMap should get removed if their submaps get emptied due to the operation. Supporting removal of outer mappings is not possible in one pass when iterating over the map that is going to be modified (unless you resort to an old loop using an Iterator).
A solution is to iterate over the childMap then:
childMap.forEach((k,v) -> mainMap.computeIfPresent(k, (d,m) -> {
v.stream().map(Integer::valueOf).forEach(m::remove);
return m.isEmpty()? null: m;
}));
Note that most complication arises from the type mismatch between your two maps, so a conversion between the childMap’s Strings to the mainMap’s Integers has to be performed. If they had the same type, e.g. if childMap was a Map<LocalDate, List<Integer>> or mainMap was Map<LocalDate, Map<String, List<String>>>, the solution not removing empty maps was as simple as
mainMap.forEach((k, v) -> v.keySet().removeAll(childMap.getOrDefault(k, emptyList())));
and the solution which will also remove empty mappings from the outer map:
childMap.forEach((k,v) -> mainMap.computeIfPresent(k, (d,m) -> {
m.keySet().removeAll(v);
return m.isEmpty()? null: m;
}));
The code above may remove mappings to an initially empty map. If you want to remove mappings only if the map has been emptied during this operation, i.e. not touch those which were empty to begin with, the code becomes even simpler:
childMap.forEach((k,v) ->
mainMap.computeIfPresent(k, (d,m) -> m.keySet().removeAll(v) && m.isEmpty()? null: m));

Related

How to convert List of Map into Map of List using Java Streams [duplicate]

This question already has answers here:
Create a map from a list of maps
(2 answers)
Closed 2 years ago.
I have List of Maps as
[{"a"="10","b"="20"},{"a"="12","b"="22"},{"a"="14","b"="24"}]
And I want a Map as
{"a"=["10","12","14"],"b"=["20","22","24"]}
You can do this without having to instantiate an additional data structure. You first map the Maps to Map.Entry and then group by key.
var listOfMaps = List.of(
Map.of("a", 10, "b", 20),
Map.of("a", 12, "b", 22),
Map.of("a", 14, "b", 24)
);
var mapOfLists = listOfMaps.stream()
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(
Map.Entry::getValue,
Collectors.toList()
)
));
System.out.println(mapOfLists);
Output:
{a=[10, 12, 14], b=[20, 22, 24]}
You can make use of Group By to achieve this. Here is an example.
First we take a stream of list using list.stream() and then we make a stream of each Map's Entry using flatMap. Now that we have a stream of each Entry(Key-value pair), we use groupingBy to collect them as a group.
First argument of groupingBy is functions what derive the key , Second argument is optional its a map factory which will be used to create the map, in case you want to have a sorted map you can use TreeMap::new , Now third param is reducer part where you tell what will be the grouped value and how it has to be collected(in our case we want it into a list)
Map<String, String> ma = new java.util.HashMap<>();
ma.put("a", "10");
ma.put("b", "20");
Map<String, String> ma3 = new java.util.HashMap<>();
ma3.put("a", "15");
ma3.put("b", "17");
ma3.put("c", "19");
List<Map<String, String>> list = Arrays.asList(ma, ma3);
Map<String, List<String>> lm = list.stream().flatMap(x -> x.entrySet().stream()).collect(Collectors
.groupingBy(Entry::getKey, HashMap::new, Collectors.mapping(Entry::getValue, Collectors.toList())));
System.out.println(lm);
Output looks like
{a=[10, 15], b=[20, 17], c=[19]}
Map<String,List<String>> map=new HashMap<>();
list.stream().flatMap(map->map.entrySet().stream()).forEach(entry->{
map.putIfAbsent(entry.getKey(),new ArrayList<>());
map.get(entry.getKey()).add(entry.getValue());
});
It is not a one-liner because I needed two statements in the forEach and I don't like to put something like that in one line but the OP did not ask for a one-liner.
It gets all entries of the inner Map as a Stream at first (flatMap).
After that, it iterates over them doing two actions:
If no List exists for the entry value, a new one is added.
The value of the entry is added to the inner list of the entry.
You can make use of the remapping function in Map#merge.
final Map<String,List<String>> result = new HashMap<>();
maps.forEach(map -> map.entrySet().forEach(entry ->
result.merge(entry.getKey(), Arrays.asList(entry.getValue()),
(a,b)->Stream.concat(a.stream(),b.stream()).collect(Collectors.toList()))));

List<Item> to Map<Type, List<Item>>, sort keys, sort values, and process

I have a large list of items that I need to convert into a map of items of same type:
List<Item> items = //10^6 items of different types
Map<Type, List<Item>> itemsByType = new ConcurrentHashMap<>();
for (Item item : items) {
itemsByType.computeIfAbsent(
item.getType(),
i -> new ArrayList<>()
).add(item);
}
Each type is then ordered by long type identifier; and, each list of type-items is ordered by long item identifier. And, finally, the ordered list is processed.
This works fine, but I'm wondering if there's a more efficient way to do all of this...?
You can use java-8 groupingBy
Map<Type, List<Item>> itemsByType = items.stream()
.sorted(Comparator) //for any sorting you can use sorted with comparator
.collect(Collectors.groupingBy(Item::getType));
If you want ConcurrentHashMap you can use groupingByConcurrent
ConcurrentMap<Type, List<Item>> itemsByType = items.stream()
.collect(Collectors.groupingByConcurrent(Item::getType));
You can use the overloaded groupingBy with TreeMap so the map is already sorted based on key
TreeMap<Type, List<Item>> map = list
.stream()
.collect(Collectors.groupingBy(
Item::Type,
() -> new TreeMap<>(Comparator.comparingLong(Type::getId)),
Collectors.toList()));
You can also collect the map with sorted keys and sorted values in one chain
Map<Type, List<Item>> str = list1.stream()
.collect(
Collectors.groupingBy(
Item::Type,
() -> new TreeMap<>(Comparator.comparingLong(Type::getId)),
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingLong(Item::getId))
.collect(Collectors.toList()))));
You could use a MultiMap, e.g., guava's. Here is their code example:
ListMultimap<String, String> multimap = ArrayListMultimap.create();
for (President pres : US_PRESIDENTS_IN_ORDER) {
multimap.put(pres.firstName(), pres.lastName());
}
for (String firstName : multimap.keySet()) {
List<String> lastNames = multimap.get(firstName);
out.println(firstName + ": " + lastNames);
}
... produces output such as:
Zachary: [Taylor]
John: [Adams, Adams, Tyler, Kennedy] // Remember, Quincy!
George: [Washington, Bush, Bush]
Grover: [Cleveland, Cleveland] // Two, non-consecutive terms, rep'ing NJ!
...
A TreeMultimap has sorted keys and values, which is what you want, if I understood your title correctly.
A Multimap is particularly useful in case you need to check if a certain value is present for a certain key, because that is supported without getting the collection for the key and then searching that:
multimap.containsEntry("John", "Adams");

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!

Using Java 8 streams, how to transform Map<String, Map<String, List<Person>>> to Map<Integer, Map<String, Integer>>?

So I have a Map<String, Map<String, List<Person>>> and I want to transform it to Map<Integer, Map<String, Integer>> where the first Integer key represents numbers 1,2,3.....(the index of the current entry taken from the first map) and the second Integer gives me the size of the List<Person>.
The String in the inner map remains the same....
First I just tried to replace the second Integer in the map with the size of the list but it does not work.....
Map<String, Map<String, List<Person>>> map2 =...
Map<String, Map<String, Integer>> map = map2.entrySet().stream().collect(
Collectors.groupingBy(p -> p.getKey(), Collectors.toMap(p -> p.getValue().getKey(), p->p.getValue().values().size())));
By definition, a map doesn't have an index. You're probably trying to give the entries of the map a number starting from 1 based on the insertion order. If you're using a LinkedHashMap there's a way to accomplish this.
However, a HashMap makes no guarantees as to the order of the map. Also, it does not guarantee that the order will remain constant over time; So, giving these entries a number will not always yield the expected outcome.
That said, you might only want to number the entries in the map from 1 to n where n is the size of the map and are not concerned in the insertion order of entries as mentioned above i.e. the first inserted entry doesn't necessarily have to be numbered as 1 and the second inserted entry doesn't necessarily have to be numbered as 2 etc..
Now, to the solution:
You can have a List<Map<String, Integer>> as a temporary result set and when you need the numbering of the entries simply loop over the elements and get the numbering with i + 1 where i is the control variable.
example assuming we have this list:
List<Map<String, Integer>> temporaryResult = myMap.values()
.stream()
.map(a -> a.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
c -> c.getValue().size()))
)
.collect(Collectors.toList());
we can then get a Map<Integer, Map<String, Integer>> with:
Map<Integer, Map<String, Integer>> resultSet =
IntStream.range(0, temporaryResult.size())
.mapToObj(i -> new AbstractMap.SimpleEntry<>(i + 1,
temporaryResult.get(i)))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey,
AbstractMap.SimpleEntry::getValue));
Note -
If the numbering should be based on insertion order this code will only yield the expected outcome if the map you're using maintains insertion order.
If the numbering is not necessarily based on insertion order of the entries in the map then this solution will work regardless of the map implementation being used.
I'd use a list to keep the order of the entries from the initial map, no need to store an index as a key of the map.
This will go through all the values of your initial map, map the Map to the one awaited with the size() of the list and collect everything into a List
List<Map<String, Integer>> map =
map2.values()
.stream()
.map(m-> m.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size())))
.collect(Collectors.toList());
If you really need a Map for any reason, the following code should work too
int i = 1;
Map<Integer, Map<String, Integer>> newMap = new HashMap<>();
for (Map.Entry entry : map2.entrySet()) {
Map<String, Integer> map = entry.value().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size())
newMap.put(i++, map);
}

Java 8 groupingby Into map that contains a list

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.

Categories

Resources