Java 8+ Mapping a Map of Lists - java

I have a Map of Lists defined as such:
Map<Date,List<TimesheetContribution>> groupedByDate;
The class TimesheetContribution has a method getHours() which returns double.
What I want is:
Map<Date, Double> hoursMap = groupedByDate.entrySet().stream()...
Where the Map Values are the total hours from the TimesheetContribution instances.
The only way I can think of is something like this:
Map<Date, Double> toilAmounts = groupedByDate.entrySet().stream()
.collect(Collectors.toMap(Function.identity(), value -> ???));
As you can see, I run into trouble when attempting to define the value mapper, and I'd need a nested stream, about which I am not comfortable.
Any suggestions? Or will I have to do this the old-fashioned way?

You can do that as :
Map<Date, Double> hoursMap = groupedByDate.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, // for a key and not an entry
e -> e.getValue().stream()
.mapToDouble(TimesheetContribution::getHours)
.sum()));

Related

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.

What is the easiest way to change the inner value type in a nested map in java?

I have a nested map Map<String, Map<String, List<ObjectA>>> passed to me, and I want to change it to type Map<String, Map<String, Set<ObjectA>>>, what is the easiest way to do so in Java using stream? I have tried to use Collectors.groupingBy but can't get it working.
The best way is you have to iterate through each entry in outer map and inner map, and then convert the inner map entry value List<ObjectA> to Set<ObjectA>
Map<String, Map<String, Set<ObjectA>>> resultMap = map.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, val -> new HashSet<>(val.getValue())))));
Note : If you are converting List to HashSet then you will not maintain same order, so you can choose LinkedHashSet over HashSet to maintain order
Map<String, Map<String, Set<ObjectA>>> resultMap = map.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, val -> new LinkedHashSet<>(val.getValue())))));

convert map<String,Map<Long,customeObject>> to list<customeobject> using java 8

can somebody help me converting Map<String, map<Long, Set<PanelData>>> to List<PanelData>?
Backgroud: as part of my task I have grouped the PanelData object on two different attributes and the end result is the above map. PanelData is just a POJO with getter and setter.
To convert a Map<String,Map<Long,CustomObject>> to List<CustomObject>, you can do it somewhat like this:
Map<String,Map<Long,CustomObject>> input = ...
List<CustomObject> output = new ArrayList<>();
input.forEach((key, value) -> output.addAll(value.values()));
You can get stream from entrySet and use flatMap to make another stream from values:
map.entrySet()
.stream()
.map(Map.Entry::getValue)
.map(Map::entrySet)
.flatMap(Set::stream)
.map(Map.Entry::getValue)
.flatMap(Set::stream)
.collect(Collectors.toList());

Transforming list values of a map to their mean value by an attribute

I start with Map<String,List<Rating>>. Rating has a method int getValue().
I want to end up with Map<String,Integer> where the Integer value is the mean value of all the Rating.getValue() values grouped by the key from the original Map<String,List<Rating>>.
I would be pleased to receive some ideas on how to tackle this.
Performing aggregation operations on a collection of integers can be done with IntStream methods. In your case, average seems like the right method to use (notice that it returns a Double, not Integer, which seems like a better choice).
What you want is to convert each entry of the original map to an entry in a new map, where the key remains the same, and the value is the average of the values of the List<Rating> elements. Generating the output map can be done using a toMap Collector.
Map<String,Double> means =
inputMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e->e.getValue()
.stream()
.mapToInt(Rating::getValue)
.average()
.orElse(0.0)));
It can be done using averagingInt as next:
Map<String, Double> means =
map.entrySet()
.stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().collect(
Collectors.averagingInt(Rating::getValue)
)
)
);
Assuming that you would like to go a little bit further and you need more statistics like count, sum, min, max and average, you could consider using summarizingInt instead, you will then get IntSummaryStatistics instead of a Double
Map<String, IntSummaryStatistics> stats =
map.entrySet()
.stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().collect(
Collectors.summarizingInt(Rating::getValue)
)
)
);

Java 8 stream API - is there standard method for processing each value in Map to different type?

I learn Java 8 Lambda Expressions and stream API. In order to understand I try to make expression analog for SQL quesry:
select department, avg(salary) from employee group by department
for:
private static class Employee {
public String name;
public String department;
public int salary;
}
Solution present in official tutorial:
empls.stream().collect(
Collectors.groupingBy(
x -> x.department,
Collectors.averagingInt(x -> x.salary)))
Before I found this solution my strategy to calculate average with grouping:
Map<String, List<Employee>> tmp =
empls.stream().collect(Collectors.groupingBy(x -> x.department));
and applying functor to each value. But in Map interface there are no method to transform value into different type. In my case reduce List to Double. Standard SE API provide only method replaceAll() that convert value to same type...
What Java 8 style method/trick/one-liner to convert Map value into different type? Worked like pseudo-code:
Map<K, V2> map2 = new HashMap<>();
for (Map.Entry<K, V1> entry : map1.entrySet()) {
map2.add(entry.getKey(), Function<V1, V2>::apply(entry.getValue()));
}
You want:
Map<K, V2> map2 =
map1.entrySet().stream()
.collect(toMap(Map.Entry::getKey,
e -> f.apply(e.getValue()));
where f is a function from V to V2.
Here working solution for exactly my example (here stream over stream loop for inner List<Employee>):
Map<String, Double> mapOfAverageSalaryByDepartment =
empls.stream().collect(Collectors.groupingBy(Employee::getDepartment))
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().mapToInt(Employee::getSalary)
.average().getAsDouble()));

Categories

Resources