Is it possible to use foreach in groupingBy in java stream? - java

I have a list with books. There is:
List(string) of authors
title(string) of book
and rating (double)
I would like to calculate average for each author of him books.
Problem is the list of authors, if I get one author for one book, It will be no problem. I would to solve it like this:
Map<String, Double> result = books.stream()
.collect(Collectors
.groupingBy(Book::getAuthor, TreeMap::new, Collectors
.averagingDouble(Book::getRating)
Anyone have solution?

I would map to a SimpleEntry containing each author along with their book to make it easier to group.
With this one can maintain both the author along with the book object thus enabling one to extract any information from the book object upon passing the downstream collector.
example:
Map<String, Double> resultSet =
books.stream()
.flatMap(book -> book.getAuthors()
.stream()
.map(author -> new AbstractMap.SimpleEntry<>(author, book)))
.collect(Collectors.groupingBy(AbstractMap.SimpleEntry::getKey,
TreeMap::new,
Collectors.averagingDouble(e -> e.getValue().getRating())));

Map<String, Long> result = books.stream()
.flatMap(i -> i.getAuthors().stream())
.collect(Collectors.groupingBy(Function.identity(),
Collectors.counting()));
flatten authors and groupingBy identity with counting can achieve this.

Related

How to convert a csv into HashMap with java streams without creating entry in intermediate operation?

I'm writing a junit test where I want to import the expected result from a csv file, as a HashMap.
The following works, but I find it kind boilerplate that I first create a MapEntry.entry(), which I than collect into a new HashMap.
csv:
#key;amount
key1;val1
key2;val2
...
keyN;valN
test:
Map<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line -> MapEntry.entry(line.split(",")[0], line.split(",")[1]))
.collect(Collectors.toMap(Map.Entry::getKey, item -> new BigDecimal(item.getValue())));
Especially I'm looking for a oneliner solution like this. I mean: can I prevent having to create a MapEntry.entry explicit before again collecting it to a hashmap?
Can this be done better? Or is there even any junit utility that can already read a csv?
You don't need to create entry, you can split the line into array using map function and then use Collectors.toMap
Map<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line->line.split(","))
.filter(line->line.length>1)
.collect(Collectors.toMap(key->key[0], value -> new BigDecimal(value[1])));
If you want to collect entries into a specific type you can use overloaded Collectors.toMap with mapSupplier
Returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements.
HashMap<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line->line.split(","))
.filter(line->line.length>1)
.collect(Collectors.toMap(key->key[0], value -> new BigDecimal(value[1]),(val1,val2)->val1, HashMap::new));
}
This worked for me:
Map<String, BigDecimal> result = Files.readAllLines(Paths.get("test.csv"))
.stream()
.collect(Collectors.toMap(l -> l.split(",")[0], l -> new BigDecimal(l.split(",")[1])));

How to combine Map values from parent Map with java 8 stream

I have a map inside a map which looks like this:
Map<String, Map<Integer, BigDecimal>> mapInMap; //--> with values like:
/*
"a": (1: BigDecimal.ONE),
"b": (2: BigDecimal.TEN),
"c": (1: BigDecimal.ZERO)
*/
And I would like to combine the inner maps by expecting the following result:
Map<Integer, BigDecimal> innerMapCombined; //--> with values:
/*
1: BigDecimal.ZERO,
2: BigDecimal.TEN
*/
This is my solution with predefining the combined map and using forEach:
Map<Integer, BigDecimal> combined = new HashMap<>();
mapInMap.forEach((str, innerMap) -> {
innerMap.forEach(combined::putIfAbsent);
});
But this will ignore (1: BigDecimal.ZERO).
Could you provide a 1-line solution with java 8 stream?
The issue with your problem is that as soon as you initialize your maps, and add the duplicate keys on the inner maps, you will rewrite those keys, since those maps do not accept duplicated keys. Therefore, you need to first change this:
Map<String, Map<Integer, BigDecimal>> mapInMap;
to a Map that allows duplicated keys, for instance Multimap from Google Guava:
Map<String, Multimap<Integer, BigDecimal>> mapInMap = new HashMap<>();
where the inner maps are created like this:
Multimap<Integer, BigDecimal> x1 = ArrayListMultimap.create();
x1.put(1, BigDecimal.ONE);
mapInMap.put("a", x1);
Only now you can try to solve your problem using Java 8 Streams API. For instance:
Map<Integer, BigDecimal> map = multiMap.values()
.stream()
.flatMap(map -> map.entries().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v2));
The duplicate keys conflicts are solved using mergeFunction parameter of the toMap method. We explicitly express to take the second value (v1, v2) -> v2 in case of duplicates.
Problem:
To address why your current solution doesn't work is because Map#putIfAbsent method only adds and doesn't replace a value in a map if is already present.
Solution using for-each:
Map#put is a way to go, however its limitation is that you cannot decide whether you want to keep always the first value for such key, calculate a new one or use always the last value. For such reason I recommend to use either a combination of Map#computeIfPresent and Map#putIfAbsent or better a method that does all that at once which is Map#merge(K, V, BiFunction) with a BiFunction remappingFunction:
remappingFunction - the function to recompute a value if present
Map<Integer, BigDecimal> resultMap = new HashMap<>();
for (Map<Integer, BigDecimal> map: mapInMap.values()) {
for (Map.Entry<Integer, BigDecimal> entry: map.entrySet()) {
resultMap.merge(entry.getKey(), entry.getValue(), (l, r) -> r);
}
}
Solution using Stream API:
To rewrite it in the Stream-alike solution, the approach would be identical. The only difference is the declarative syntax of Stream API, however, the idea is very same.
Just flatMap the structure and collect to a map with a Collector.toMap(Function, Function, BinaryOperator using BinaryOperator mergeFunction to merge duplicated keys.
mergeFunction - a merge function, used to resolve collisions between values associated with the same key, as supplied to Map.merge(Object, Object, BiFunction)
Map<Integer, BigDecimal> resultMap = mapInMap.values().stream()
.flatMap(entries -> entries.entrySet().stream())
.collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (l, r) -> r));
Note: #dreamcrash also deserves a credit for his good Stream API answer in terms of speed.
Result:
{1=1, 2=10} is the result when you pring out such map (note that BigDecimal is printed as a number). This output matches your expected output.
1=BigDecimal.ZERO
2=BigDecimal.TEN
Notice the similarities between Map#merge(K, V, BiFunction) and Collector.toMap(Function, Function, BinaryOperator that use a very similar approach to the same result.

Extract all objects that are part of a hash map List with Streams

I have a HashMap that consists of a list of objects:
Map<String, List<Employee>> map;
I would like to extract all the employee objects from each list into a new list.
I tried to do the following but either does not give me what I want or is an error.
List<Asset> as = selectedAssetsMap.values().stream().findAny().get().stream().collect(Collectors.toList());
List<Asset> s = selectedAssetsMap.values().stream().forEach(l -> 1.stream().collect(Collectors.toList()));
List<Asset> a1 = selectedAssetsMap.values().stream().map(l -> l.collect(Collectors.toList()));
The first version gives me only the first set of Employee objects. The second one are wrong... Any help/direction will be very helpful
You may use the flatMap operator like this.
List<Employee> allEmps = map.values().stream()
.flatMap(List::stream)
.collect(Collectors.toList());

Elegant way to flatMap Set of Sets inside groupingBy

So I have a piece of code where I'm iterating over a list of data. Each one is a ReportData that contains a case with a Long caseId and one Ruling. Each Ruling has one or more Payment. I want to have a Map with the caseId as keys and sets of payments as values (i.e. a Map<Long, Set<Payments>>).
Cases are not unique across rows, but cases are.
In other words, I can have several rows with the same case, but they will have unique rulings.
The following code gets me a Map<Long, Set<Set<Payments>>> which is almost what I want, but I've been struggling to find the correct way to flatMap the final set in the given context. I've been doing workarounds to make the logic work correctly using this map as is, but I'd very much like to fix the algorithm to correctly combine the set of payments into one single set instead of creating a set of sets.
I've searched around and couldn't find a problem with the same kind of iteration, although flatMapping with Java streams seems like a somewhat popular topic.
rowData.stream()
.collect(Collectors.groupingBy(
r -> r.case.getCaseId(),
Collectors.mapping(
r -> r.getRuling(),
Collectors.mapping(ruling->
ruling.getPayments(),
Collectors.toSet()
)
)));
Another JDK8 solution:
Map<Long, Set<Payment>> resultSet =
rowData.stream()
.collect(Collectors.toMap(p -> p.Case.getCaseId(),
p -> new HashSet<>(p.getRuling().getPayments()),
(l, r) -> { l.addAll(r);return l;}));
or as of JDK9 you can use the flatMapping collector:
rowData.stream()
.collect(Collectors.groupingBy(r -> r.Case.getCaseId(),
Collectors.flatMapping(e -> e.getRuling().getPayments().stream(),
Collectors.toSet())));
The cleanest solution is to define your own collector:
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collector.of(HashSet::new,
(s, r) -> s.addAll(r.getRuling().getPayments()),
(s1, s2) -> { s1.addAll(s2); return s1; })
));
Two other solutions to which I thought first but are actually less efficient and readable, but still avoid constructing the intermediate Map:
Merging the inner sets using Collectors.reducing():
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collectors.reducing(Collections.emptySet(),
r -> r.getRuling().getPayments(),
(s1, s2) -> {
Set<Payment> r = new HashSet<>(s1);
r.addAll(s2);
return r;
})
));
where the reducing operation will merge the Set<Payment> of entries with the same caseId. This can however cause a lot of copies of the sets if you have a lot of merges needed.
Another solution is with a downstream collector that flatmaps the nested collections:
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collectors.collectingAndThen(
Collectors.mapping(r -> r.getRuling().getPayments(), Collectors.toList()),
s -> s.stream().flatMap(Set::stream).collect(Collectors.toSet())))
);
Basically it puts all sets of matching caseId together in a List, then flatmaps that list into a single Set.
There are probably better ways to do this, but this is the best I found:
Map<Long, Set<Payment>> result =
rowData.stream()
// First group by caseIds.
.collect(Collectors.groupingBy(r -> r.case.getCaseId()))
.entrySet().stream()
// By streaming over the entrySet, I map the values to the set of payments.
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().stream()
.flatMap(r -> r.getRuling().getPayments().stream())
.collect(Collectors.toSet())));

Java API Streams collecting stream in Map where value is a TreeSet

There is a Student class which has name, surname, age fields and getters for them.
Given a stream of Student objects.
How to invoke a collect method such that it will return Map where keys are age of Student and values are TreeSet which contain surname of students with such age.
I wanted to use Collectors.toMap(), but got stuck.
I thought I could do like this and pass the third parameter to toMap method:
stream().collect(Collectors.toMap(Student::getAge, Student::getSurname, new TreeSet<String>()))`.
students.stream()
.collect(Collectors.groupingBy(
Student::getAge,
Collectors.mapping(
Student::getSurname,
Collectors.toCollection(TreeSet::new))
))
Eugene has provided the best solution to what you want as it's the perfect job for the groupingBy collector.
Another solution using the toMap collector would be:
Map<Integer, TreeSet<String>> collect =
students.stream()
.collect(Collectors.toMap(Student::getAge,
s -> new TreeSet<>(Arrays.asList(s.getSurname())),
(l, l1) -> {
l.addAll(l1);
return l;
}));

Categories

Resources