Collect results of a map operation in a Map using Collectors.toMap or groupingBy - java

I've got a list of type List<A> and with map operation getting a collective list of type List<B> for all A elements merged in one list.
List<A> listofA = [A1, A2, A3, A4, A5, ...]
List<B> listofB = listofA.stream()
.map(a -> repo.getListofB(a))
.flatMap(Collection::stream)
.collect(Collectors.toList());
without flatmap
List<List<B>> listOflistofB = listofA.stream()
.map(a -> repo.getListofB(a))
.collect(Collectors.toList());
I want to collect the results as a map of type Map<A, List<B>> and so far trying with various Collectors.toMap or Collectors.groupingBy options but not able to get the desired result.

You can use the toMap collector with a bounded method reference to get what you need. Also notice that this solution assumes you don't have repeated A instances in your source container. If that precondition holds this solution would give you the desired result. Here's how it looks.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB);
If you have duplicate A elements, then you have to use this merge function in addition to what is given above. The merge function deals with key conflicts if any.
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(Function.identity(), repo::getListofB,
(a, b) -> {
a.addAll(b);
return a;
}));
And here's a much more succinct Java9 approach which uses the flatMapping collector to handle repeated A elements.
Map<A, List<B>> aToBmap = listofA.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.flatMapping(a -> getListofB(a).stream(),
Collectors.toList())));

It would be straight forward,
listofA.stream().collect(toMap(Function.identity(), a -> getListofB(a)));

In this answer, I'm showing what happens if you have repeated A elements in your List<A> listofA list.
Actually, if there were duplicates in listofA, the following code would throw an IllegalStateException:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
repo::getListofB);
The exception might be thrown because Collectors.toMap doesn't know how to merge values when there is a collision in the keys (i.e. when the key mapper function returns duplicates, as it would be the case for Function.identity() if there were repeated elements in the listofA list).
This is clearly stated in the docs:
If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.
The docs also give us the solution: in case there are repeated elements, we need to provide a way to merge values. Here's one such way:
Map<A, Collection<B>> resultMap = listofA.stream()
.collect(Collectors.toMap(
Function.identity(),
a -> new ArrayList<>(repo.getListofB(a)),
(left, right) -> {
left.addAll(right);
return left;
});
This uses the overloaded version of Collectors.toMap that accepts a merge function as its third argument. Within the merge function, Collection.addAll is being used to add the B elements of each repeated A element into a unqiue list for each A.
In the value mapper function, a new ArrayList is created, so that the original List<B> of each A is not mutated. Also, as we're creating an Arraylist, we know in advance that it can be mutated (i.e. we can add elements to it later, in case there are duplicates in listofA).

To collect a Map where keys are the A objects unchanged, and the values are the list of corresponding B objects, you can replace the toList() collector by the following collector :
toMap(Function.identity(), a -> repo.getListOfB(a))
The first argument defines how to compute the key from the original object : identity() takes the original object of the stream unchanged.
The second argument defines how the value is computed, so here it just consists of a call to your method that transforms a A to a list of B.
Since the repo method takes only one parameter, you can also improve clarity by replacing the lambda with a method reference :
toMap(Function.identity(), repo::getListOfB)

Related

Java Streams - How to preserve the initial Order of elements while using Collector groupingBy()

I'm trying to convert a list of objects into a list of lists of objects, i.e. List<T> into List<List<T>> and group them by a list of strings used as sorting keys.
The problem is that after the groupingBy(), the return value is a map, and that map a different order of values.
What can I do to preserve the initial order of elements in the list groupedTreesList?
My code:
Map<List<String>,List<QueryTreeProxy>> groupedMap = treesProxyList.stream()
.collect(Collectors.groupingBy(
QueryTreeProxy::getMappingKeys,
Collectors.toList()
));
List<List<QueryTreeProxy>> groupedTreesList = groupedMap.values().stream().toList();
groupingBy(), the return value is a map and that map output a different order of values list
You can use another flavor of groupingBy(classifier, mapFactory, downstream), which allows you to specify the type of the Map. And we can use LinkedHashMap to preserve the initial order.
And there's no need to generate the second stream, you can use parameterized contractor of ArrayList instead.
Map<List<String>, List<QueryTreeProxy>> groupedMap = treesProxyList.stream()
.collect(Collectors.groupingBy(
QueryTreeProxy::getMappingKeys,
LinkedHashMap::new,
Collectors.toList()
));
List<List<QueryTreeProxy>> groupedTreesList = new ArrayList<>(groupedMap.values());
Besides that, using a mutable object as a key isn't a good practice.

Collector to join incoming lists

I have a method that receives a collector. The collector should combine incoming lists:
reducing(Lists.newArrayList(), (container, list) -> {
container.addAll(list);
return container;
})
This seems like a very common scenario to me, and I believe Java's own collector should have something to cover this use case, but I cannot remember what. Besides, I want it to return ImmutableList.
To obtain a "flattened" list of elements from a nested collection firstly you need to apply flatMapping() which takes a function that turns an element to a stream and a collector as second argument. And to get an immutable list apply toUnmodifiableList() as a downstream collector inside the flatMapping() (as already mentioned by racraman and hfontanez in their comments, since they pointed that earlier the credits for this answer should belong to them).
List<List<Integer>> nestedList = List.of(
List.of(1, 2),
List.of(3, 4)
);
nestedList.stream()
.collect(Collectors.flatMapping(List::stream,
Collectors.toUnmodifiableList()))
.forEach(System.out::println);
output
1
2
3
4
A more common use-case for Collector.flatMapping() is when each element of the stream holds a reference to a collection, rather than being a collection itself.
Consider the following scenario.
We have a collection of Orders. Each Order has an id and a collection of Items. And we want to obtain a map from this collection of orders so that based on the order id we can get a list of items.
In this situation Collector.flatMapping() is indispensable because by applying the flatMap() in the middle of the stream we will lose access to the orderId in the collect(). Hence, flattening should happen inside the collect() operation.
A method that implements the logic described above might look this:
public Map<String, List<Item>> getItemsByOrderId(Collection<Order> orders) {
return orders.stream()
// some logic here
.collect(Collectors.groupingBy(Order::getId,
Collectors.flatMapping(order ->
order.getItems().stream().filter(somePredicate),
Collectors.toList())));
}
We can also do flatMap() operation and toList() collector, to collect multiple incoming lists to one resultant list.
List<String> joinedList = Stream.of(
asList("1", "2"),
asList("3", "4")).flatMap(list -> list.stream()
)
.collect(toList());
System.out.println("Printing the list named as joinedList: " + joinedList);
Output:
Printing the list named joinedList: [1, 2, 3, 4]

How to get an object instead of a list after applying groupingby on a list of Objects

I am doing a group by on a list of Objects as shown in the below code
Map<String, List<InventoryAdjustmentsModel>> buildDrawNumEquipmentMap = equipmentsAndCargoDetails.stream().
collect(Collectors.groupingBy(InventoryAdjustmentsModel :: getBuildDrawNum));
Now I know the values for all the keys would have only one element, so how can I reduce it to just
Map<String, InventoryAdjustmentsModel>
instead of having to iterate through or get the 0th element for all the keys.
You may use the toMap collector with a merge function like this.
Map<String, InventoryAdjustmentsModel> resultMap = equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
e -> e, (a, b) -> a));
Try it like this. By using toMap you can specify the key and the value. Since you said there were no duplicate keys this does not include the merge method. This means you will get an error if duplicate keys are discovered. Something I presumed you would want to know about.
Map<String, InventoryAdjustmentsModel> buildDrawNumEquipmentMap =
equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
model->model));

Java 8 use streams to distinct objects with duplicated field value

I have a list of objects. Each object has three fields: id, secNumber and type. Type is enum which can have values 'new' or 'legacy'. Sometimes it happens that there are objects in that list which have the same secNumber a but different type.
in such a situation, I need to remove the one with type 'legacy'. How to do it using Java 8 streams?
use toMap with something like this:
Collection<T> result = list.stream()
.collect(toMap(T::getSecNumber,
Function.identity(),
(l, r) -> l.getType() == Type.LEGACY ? r : l))
.values();
where T is the class that contains secNumber, id etc.
The keyMapper (T::getSecNumber) extracts each secNumber from each object.
The valueMapper (Function.identity()) extracts the objects we want as the map values i.e. the objects from the source them selves.
The mergeFunction (l, r) -> is where we say " if two given objects have the same key i.e. getSecNumber then keep the one where their type is 'NEW' and discard the one with 'LEGACY'" and finally we call values() to accumulate the map values into a Collection.
Edit:
following #Tomer Aberbach's comment you may be looking for:
List<T> result =
list.stream()
.collect(groupingBy(T::getSecNumber))
.values()
.stream()
.flatMap(l -> l.stream().anyMatch(e -> e.getType() == Type.NEW) ?
l.stream().filter(e -> e.getType() != Type.LEGACY) :
l.stream())
.collect(toList());
The first solution using toMap assumes there can't be multiple objects with the same secNumber and type.
Assume objects is a List<ClassName> which has been declared and initialized:
List<ClassName> filteredObjects = objects.stream()
.collect(Collectors.groupingBy(ClassName::getSecNumber))
.values().stream()
.flatMap(os -> os.stream().anyMatch(o -> o.getType() == Type.NEW) ?
os.stream().filter(o -> o.getType() != Type.LEGACY) :
os.stream()
).collect(Collectors.toList());
I made the assumption that objects of type Type.LEGACY should only be filtered out if there exists another object of type Type.NEW which has the same secNumber. I also made the assumption that you could have multiple objects of the same type and secNumber and that those may need to be retained.
Note that the collect(Collectors.groupingBy(ClassName::getSecNumber)) returns a map from whatever type secNumber is to List<ClassName> so calling values() on it returns a Collection<List<ClassName>> which represents a collection of the groupings of objects with the same secNumber.
The flatMap part takes each grouping by secNumber, checks if the grouping has at least one object of Type.NEW, and if so, filters out the objects of type Type.LEGACY, otherwise it just passes along the objects to be flattened into the final List<ClassName>. This is primarily so that if a grouping only has objects of type Type.LEGACY then they are not left out of the final collection.

Java 8 HashMap<Integer, ArrayList<Integer>>

I am new Java 8 and want to sort a Map based on Key and then sort each list within values.
I tried to look for a Java 8 way to sort Keys and also value.
HashMap> map
map.entrySet().stream().sorted(Map.Entry.comparingByKey())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (e1, e2) -> e2, LinkedHashMap::new));
I am able to sort the Map and I can collect each values within map to sort but is their a way we can do in java 8 and can both be combined.
To sort by key, you could use a TreeMap. To sort each list in the values, you could iterate over the values of the map by using the Map.values().forEach() methods and then sort each list by using List.sort. Putting it all together:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.values().forEach(list -> list.sort(null)); // null: natural order
This sorts each list in-place, meaning that the original lists are mutated.
If instead you want to create not only a new map, but also new lists for each value, you could do it as follows:
Map<Integer, List<Integer>> sortedByKey = new TreeMap<>(yourMap);
sortedByKey.replaceAll((k, originalList) -> {
List<Integer> newList = new ArrayList<>(originalList);
newList.sort(null); // null: natural order
return newList;
});
EDIT:
As suggested in the comments, you might want to change:
sortedByKey.values().forEach(list -> list.sort(null));
By either:
sortedByKey.values().forEach(Collections::sort);
Or:
sortedByKey.values().forEach(list -> list.sort(Comparator.naturalOrder()));
Either one of the two options above is much more expressive and shows the developer's intention in a better way than using null as the comparator argument to the List.sort method.
Same considerations apply for the approach in which the lists are not modified in-place.

Categories

Resources