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

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.

Related

get an ordered list of objects from 2 maps sharing same properties in JAVA

I have a map with order(rank) I expect
sortedMap{deviceId:rank}:
{0:"id0"},
{1,"id1"},
{2:"id2"},
{3:"id3"},
having another map that needs to be sorted
{"id3":object3},
{"id1":object1},
{"id0":object0},
{"id2":object2}
I expected having list like
{object0},
{object1},
{object2},
{object3}
i tried with code like below, but it seems wrong with the list adding part in my code, and i don't know any better solution for it:
private List<MyObject> getSortedResultList(Map<String, Integer> sortedMap, Map<String, Object> toSortMap) {
List<MyObject> sortedResultList = null;
for (Integer rank : sortedMap.keySet()) {
for (int i = 0; i < sortedMap.size(); i++) {
sortedResultList.add(i, (MyObject) toSortMap.get(sortedMap.get(rank)));
}
}
return sortedResultList;
}
Tried searched online for hours, got no clues to solve it because of my weak datastructure knowledge and decided asking for help from here, thanks advanced if any suggestions could be raised.
Here is one way to do it:
List<Object> sortedList = sortedMap.entrySet().stream()
.sorted(Comparator.comparingInt(Map.Entry::getValue))
.map(entry -> toSortMap.get(entry.getKey()))
.filter(Objects::nonNull)
.collect(Collectors.toList());
First the entries are sorted, then we get the corresponding object, then we filter out the null values and finally we collect everything in a list.
If sortedMap is sorted by key (e.g. it's a TreeMap) it should suffice to build the list sorted by the sortedMap's values:
private static List<MyObject> getSortedResultList(TreeMap<Integer, String> sortedMap, Map<String, MyObject> toSortMap) {
return sortedMap.values().stream()
.map(key -> toSortMap.get(key))
.collect(Collectors.toList());
}
If sortedMap needs to be sorted additionally:
private static List<MyObject> getSortedResultList2(Map<Integer, String> sortedMap, Map<String, MyObject> toSortMap) {
return sortedMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> toSortMap.get(entry.getValue()))
.collect(Collectors.toList());
}
Test:
Map<Integer, String> sorted = new TreeMap<>(Map.of(1, "id1", 0, "id0", 3, "id3", 2, "id2"));
Map<String, MyObject> toSort = Map.of(
"id3", new MyObject("object3"), "id1", new MyObject("object1"),
"id0", new MyObject("object0"), "id2", new MyObject("object2")
);
System.out.println(getSortedResultList(sorted, toSort));
System.out.println(getSortedResultList2(sorted, toSort));
Both outputs identical:
[MyObject(field=object0), MyObject(field=object1), MyObject(field=object2), MyObject(field=object3)]
[MyObject(field=object0), MyObject(field=object1), MyObject(field=object2), MyObject(field=object3)]

How to Filter a map of map based on another map using Lambda expressions

I have a Map of Map which needs to be filtered based of another Map using lambda expressions
I tried to do filter on the map and find all matches based of another map but it doesnot seem to work. It seems the values are not filtered correctly.
Is there a way I can do streams and map and put the filtering logic there?
Can someone please help
public static void main(String []args){
System.out.println("Hello World");
Map<String,List<String>> items = new HashMap<>();
List<String> ut1=new ArrayList<>();
ut1.add("S");
ut1.add("C");
List<String> ut2=new ArrayList<>();
ut2.add("M");
List<String> ut3=new ArrayList<>();
ut3.add("M");
ut3.add("C");
items .put("1010016",ut1);
items .put("1010019",ut2);
items .put("1010012",ut3);
System.out.println("Map"+items);
Map<String,Map<String,String>> sKey = new HashMap<>();
Map<String,String> utKey1 = new HashMap<>();
utKey1.put("S","1001");
utKey1.put("M","1002");
utKey1.put("C","1003");
Map<String,String> utKey2 = new HashMap<>();
utKey2.put("S","1004");
Map<String,String> utKey3 = new HashMap<>();
utKey3.put("S","1005");
utKey3.put("M","1006");
Map<String,String> utKey4 = new HashMap<>();
utKey4.put("S","1007");
utKey4.put("M","1008");
utKey4.put("C","1009");
sKey.put("1010016",utKey1);
sKey.put("1010019",utKey2);
sKey.put("1010012",utKey3);
sKey.put("1010011",utKey4);
System.out.println("Map2"+sKey);
Map<String,Map<String,String>> map3 =
sKey.entrySet().stream()
.filter(x ->
items.containsKey(x.getKey())
&& x.getValue().entrySet().stream().allMatch(y ->
items.entrySet().stream().anyMatch(list ->
list.getValue().contains(y.getKey()))))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
System.out.println("Map3"+map3);
}
the filtered map is returning as:
Map3{1010012={S=1005, M=1006}, 1010016={S=1001, C=1003, M=1002}, 1010019={S=1004}}
But the actual result should be:
Map3{1010012={M=1006}, 1010016={S=1001, C=1003}}
I will rather say this is a work around to achieve your expected output using stream.
Map<String, Map<String, String>> result =
sKey.entrySet().stream()
.filter(detail -> items.keySet().contains(detail.getKey()) &&
!Collections.disjoint(detail.getValue().keySet(), items.get(detail.getKey())))
.collect(HashMap::new,
(m,v) -> m.put(v.getKey(), v.getValue().entrySet().stream()
.filter(detail -> items.get(v.getKey()).contains(detail.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))),
HashMap::putAll);
Output
{1010012={M=1006}, 1010016={S=1001, C=1003}}

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!

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.

Java 8 Merge maps in iterator

I have an iteraror where in every iteration I´m creating a new map
Map<String, List<String>>
Now I would like to merge in every iteration the last emitted map with the new one.
If I send a list of items to getMap
{"a","a","b"}
I expect to receive a map of
["a",{"foo:a", "foo:a"}, "b",{"foo:b"}]
I try to use reduce function, but because putall only works if I use multimap and not map, is not a good option.
Here my code
public Map<String, List<String>> getMap(List<String> items){
return items().stream()
.map(item -> getNewMap(item) --> Return a Map<String, List<String>>
.reduce(new HashMap<>(), (o, p) -> {
o.putAll(p);
return o;
});
}
public Map<String, List<String>> getNewMap(String item){
Map<String, List<String>> map = new HashMap<>();
map.put(item, Arrays.asList("foo:" + item));
return map;
}
I´m looking for a no verbose way to do it.
What you want is to flat map each intermediate map to its entries and make a single map out of that.
In the following code, each item is mapped to its corresponding map. Then, each map is flat mapped to its entries and the Stream is collected into a map.
public static void main(String[] args) {
System.out.println(getMap(Arrays.asList("a", "a", "b")));
// prints "{a=[foo:a, foo:a], b=[foo:b]}"
}
public static Map<String, List<String>> getMap(List<String> items) {
return items.stream()
.map(item -> getNewMap(item))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(l1, l2) -> { List<String> l = new ArrayList<>(l1); l.addAll(l2); return l; }
));
}
public static Map<String, List<String>> getNewMap(String item) {
Map<String, List<String>> map = new HashMap<>();
map.put(item, Arrays.asList("foo:" + item));
return map;
}
In the case of multiple keys, this appends each list together.
Whenever you want to get a Map<…, List<…>> from a stream, you should first check, how the groupingBy collector fits in. In its simplest form, it receives a grouping function which determines the keys of the resulting map and will collect all elements of a group into a list. Since you want the prefix "foo:" prepended, you’ll have to customize this group collector by inserting a mapping operation before collecting the items into a list:
public static Map<String, List<String>> getMap(List<String> items) {
return items.stream().collect(Collectors.groupingBy(
Function.identity(),
Collectors.mapping("foo:"::concat, Collectors.toList())));
}
The classification function itself is as trivial as the identity function, as you want all equal elements building one group.

Categories

Resources