Java 8 Join Map with Collectors.toMap - java

I'm trying to collect in a Map the results from the process a list of objects and that it returns a map. I think that I should do it with a Collectors.toMap but I haven't found the way.
This is the code:
public class Car {
List<VersionCar> versions;
public List<VersionCar> getVersions() {
return versions;
}
}
public class VersionCar {
private String wheelsKey;
private String engineKey;
public String getWheelsKey() {
return wheelsKey;
}
public String getEngineKey() {
return engineKey;
}
}
process method:
private static Map<String,Set<String>> processObjects(VersionCar version) {
Map<String,Set<String>> mapItems = new HashMap<>();
mapItems.put("engine", new HashSet<>(Arrays.asList(version.getEngineKey())));
mapItems.put("wheels", new HashSet<>(Arrays.asList(version.getWheelsKey())));
return mapItems;
}
My final code is:
Map<String,Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.collect(Collectors.toMap()); // here I don't know like collect the map.
My idea is to process the list of versions and in the end get a Map with two items: wheels and engine but with a set<> with all different items for all versions. Do you have any ideas as can I do that with Collectors.toMap or another option?

The operator you want to use in this case is probably "reduce"
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.reduce((map1, map2) -> {
map2.forEach((key, subset) -> map1.get(key).addAll(subset));
return map1;
})
.orElse(new HashMap<>());
The lambda used in "reduce" is a BinaryOperator, that merges 2 maps and return the merged map.
The "orElse" is just here to return something in the case your initial collection (versions) is empty.
From a type point of view it gets rid of the "Optional"

You can use Collectors.toMap(keyMapper, valueMapper, mergeFunction). Last argument is used to resolve collisions between values associated with the same key.
For example:
Map<String, Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(firstSet, secondSet) -> {
Set<String> result = new HashSet<>();
result.addAll(firstSet);
result.addAll(secondSet);
return result;
}
));

To get the mapAllItems, we don't need and should not define processObjects method:
Map<String, Set<String>> mapAllItems = new HashMap<>();
mapAllItems.put("engine", car.getVersions().stream().map(v -> v.getEngineKey()).collect(Collectors.toSet()));
mapAllItems.put("wheels", car.getVersions().stream().map(v -> v.getWheelsKey()).collect(Collectors.toSet()));
Or by AbstractMap.SimpleEntry which is lighter than the Map created byprocessObjects`:
mapAllItems = car.getVersions().stream()
.flatMap(v -> Stream.of(new SimpleEntry<>("engine", v.getEngineKey()), new SimpleEntry<>("wheels", v.getWheelsKey())))
.collect(Collectors.groupingBy(e -> e.getKey(), Collectors.mapping(e -> e.getValue(), Collectors.toSet())));

Related

How to filter a HashMap with a value Predicate?

I have the following query header method:
public Map<String, List<String>> query(Predicate<String> valuePredicate)
Before this, I implementated another method with a specific column (label). It was:
public Map<String, List<String>> query(String keySelector,Predicate<String> valuePredicate) {
try {
final List<String> row = frameInfo.get(keySelector);
List<Integer> indices = IntStream.range(0, row.size()).filter(columnIndex -> valuePredicate.test(row.get(columnIndex))).boxed().collect(Collectors.toList());
Map<String, List<String>> auxMap = new HashMap<>();
for (Map.Entry<String, List<String>> entry : frameInfo.entrySet()) {
for (int columnIndex : indices) {
auxMap.putIfAbsent(entry.getKey(), new ArrayList<>());
auxMap.get(entry.getKey()).add(entry.getValue().get(columnIndex));
}
}
return auxMap;
}catch (Exception e){
return null;
}
How could I implementate the new method with just 1 argument (valuePredicate)?
It seems to me that you could do it like so. Since the predicate tests a string from a list which can be streamed, I don't see why you need to iterate the indices.
Stream the entrySet from frameInfo
then flatmap e.getValue() (a list) and apply the predicate
preserve the key and filtered value in a String array
then group based on the key
public Map<String, List<String>> queryAll(Predicate<String> valuePredicate) {
return frameInfo.entrySet().stream()
.flatMap(e -> e.getValue().stream()
.filter(valuePredicate)
.map(s -> new String[] { e.getKey(), s }))
.collect(Collectors.groupingBy(arr -> arr[0],
Collectors.mapping(arr -> arr[1],
Collectors.toList())));
}
I'm tossing this one in as well, it's a rewrite of your existing method.
it simply streams the list for the supplied key, applies the filter and populates the map. Since there is only one key, you could just return a list.
public Map<String, List<String>> query(String keySelector,
Predicate<String> valuePredicate) {
return frameInfo.get(keySelector).stream()
.filter(valuePredicate)
.collect(Collectors.groupingBy(a -> keySelector));
}
If I misunderstood something, let me know and I will try to correct it.

creating Map from List is not giving expected result

I have the two list objects as shown below, from which i'm creating the map object.
List<Class1> list1;
List<Class2> list2;
HashMap<String,String> map = new HashMap<>();
for(Class1 one : list1){
if(one.isStatus()){
map.put(one.getID(),one.getName());
}
}
//iterating second list
for(Class2 two : list2){
if(two.isPerformed()){
map.put(two.getID(),two.getName());
}
}
The above code works fine , want the above to be written using streams.
Below is the sample code using streams().
map = list1.stream().filter(one.isStatus()).collect(toMap(lst1 -> lst1.getID(), lst1.getName());
map = list2.stream().filter(...);
But the "map" is not giving the expected result when written using stream() API.
Stream concatenation Stream.concat may be applied here to avoid map.putAll
Map<String, String> map = Stream.concat(
list1.stream()
.filter(Class1::isStatus)
.map(obj -> Arrays.asList(obj.getID(), obj.getName())),
list2.stream()
.filter(Class2::isPerformed)
.map(obj -> Arrays.asList(obj.getID(), obj.getName()))
) // Stream<List<String>>
.collect(Collectors.toMap(
arr -> arr.get(0), // key - ID
arr -> arr.get(1),
(v1, v2) -> v1 // keep the first value in case of possible conflicts
));
The code above uses a merge function (v1, v2) -> v1 to handle possible conflicts when the same ID occurs several times in list1 and/or list2 to keep the first occurrence.
However, the following merge function allows joining all the occurrences into one string value (v1, v2) -> String.join(", ", v1, v2).
I'm not sure what expected result you're not seeing but I created a minimal working example that you should be able to adapt for your own use case.
public class Main {
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
Map<Integer, String> personMap = personList.stream()
.filter(Person::isStatus)
.collect(Collectors.toMap(person -> person.id, person -> person.name));
}
private static class Person {
public String name;
public int id;
public boolean isStatus() {
return true;
}
}
}
Try this,
List<Class1> list1;
List<Class2> list2;
Map<String, String> map1 = list1.stream().filter(Class1::isStatus).collect(Collectors.toMap(Class1::getId, Class1::getName));
Map<String, String> map2 = list2.stream().filter(Class2::isPerformed).collect(Collectors.toMap(Class2::getId, Class2::getName));
map1.putAll(map2);

Java 8 Stream API: how to convert a List to a Map<Long, Set> having repeated keys in the List?

I have a class with the following fields:
public class Item{
private String name;
private Long category;
private Set<Long> containers;
}
What I need to do is turn a
List<Item> items
into a
Map<Long/*categoryID*/, Set<Long/*Containers*/>>
using the Java 8 Stream API.
Right now I'm able to get the same result using Itarable and some if, like this:
List<Item> items = getItems();
Iterator<Item> itemsIterator = items.iterator();
Map<Long/*categoryID*/, Set<Long/*Containers.ID*/>> containersByCategoryMap = new HashMap<>();
while (itemsIterator.hasNext()) {
Item item = itemsIterator.next();
Long category = item.getCategory();
Set<Long> containers = item.getContainers();
if (containersByCategoryMap.containsKey(category)) {
Set<Container> containersByCategory = containersByCategoryMap.get(category);
containersByCategory.addAll(containers);
} else {
Set<Container> containersByCategory = new HashSet<>(containers);
containersByCategoryMap.put(category, containersByCategory);
}
}
How can I get the same result using the Stream API?
I tried something like this, but obviously I got duplicate keys exception, since there are multiple items for each category...
containersByCategoryMap = items.stream().collect(Collectors.toMap(item -> item.getCategory(), item -> item.getContainers()));
Since java-9 there is Collectors.flatMapping:
Map<Long, Set<Long>> map = items.stream()
.collect(Collectors.groupingBy(
Item::getCategory,
Collectors.flatMapping(x -> x.getContainers().stream(), Collectors.toSet())));
Without java-9, you could do:
Map<Long, Set<Long>> result = items.stream()
.flatMap(x -> x.getContainers().stream().map(y -> new SimpleEntry<>(x.getCategory(), y)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toSet())));
You can also do this with Map#merge:
Map<Long, Set<Long>> map2 = new HashMap<>();
items.forEach(x -> map2.merge(
x.getCategory(),
x.getContainers(),
(left, right) -> {
left.addAll(right);
return left;
}));
You might also be interested in using Guava and its SetMultimap interface (which is easier to use than Map<?, Set<?>>).
If you wanted your resulting data structure to be immutable, you could use ImmutableSetMultimap.flatteningToImmutableSetMultimap like below:
SetMultimap<Long, Long> multimap = items.stream()
.collect(ImmutableSetMultimap.flatteningToImmutableSetMultimap(
Item::getCategory, item -> item.getContainers().stream()
));

Group objects by multiple attributes with Java 8 stream API

Given we have a list of Bank, each Bank have multiple offices,
public class Bank {
private String name;
private List<String> branches;
public String getName(){
return name;
}
public List<String> getBranches(){
return branches;
}
}
For example:
Bank "Mizuho": branches=["London", "New York"]
Bank "Goldman": branches = ["London", "Toronto"]
Given a list of banks, I would have a map of bank representation for each city. In the example above, I need a result of
Map["London"] == ["Mizuho", "Goldman"]
Map["New York"] == ["Mizuho"]
Map["Toronto"] == ["Goldman"]
How can I achieve that result using Java 8 API? Using pre-Java8 is easy, but verbose.
Thank you.
Map<String, Set<Bank>> result = new HashMap<>();
for (Bank bank : banks) {
for (String branch : bank.getBranches()) {
result.computeIfAbsent(branch, b -> new HashSet<Bank>()).add(bank);
}
}
banks.flatMap(bank -> bank.getBranches()
.stream()
.map(branch -> new AbstractMap.SimpleEntry<>(branch, bank)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())));
Result would be:
{London=[Mizuho, Goldman], NewYork=[Mizuho], Toronto=[Goldman]}
You could do it using the version of Stream.collect that accepts a supplier, an accumulator function and a combiner function, as follows:
Map<String, List<Bank>> result = banks.stream()
.collect(
HashMap::new,
(map, bank) -> bank.getBranches().forEach(branch ->
map.computeIfAbsent(branch, k -> new ArrayList<>()).add(bank)),
(map1, map2) -> map2.forEach((k, v) -> map1.merge(k, v, (l1, l2) -> {
l1.addAll(l2);
return l1;
})));
I think solution provided by #JB Nizet is one of the most simple/efficient solutions. it can also be rewritten by forEach
banks.forEach(b -> b.getBranches().forEach(ch -> result.computeIfAbsent(ch, k -> new ArrayList<>()).add(b)));
Another short solution by Stream with abacus-common
Map<String, List<Bank>> res = Stream.of(banks)
.flatMap(e -> Stream.of(e.getBranches()).map(b -> Pair.of(b, e)))
.collect(Collectors.toMap2());

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