brain storm
brain storm

Reputation: 31252

Average counts from HashMap using java 8 stream API?

I have a Map of the following type

public class MapUtils {

    private Map<String, Integer> queryCounts = new HashMap<>();

public void averageCounters(){

    int totalCounts = queryCounts.values().stream().reduce(0, Integer::sum);
    queryCounts = queryCounts.entrySet()
                             .stream()
                             .collect(Collectors.toMap(
                                 Map.Entry::getKey,
                                 (Map.Entry::getValue)/totalCounts
    ));
}

This does not compile and shows error in this line (Map.Entry::getValue)/totalCounts. How do I fix this? Is there a better way to get achieve average over Map using Java 8 API?

EDIT: Is this a better approach?

queryCounts.entrySet()
           .forEach(entry -> queryCounts.put(entry.getKey(),
                                   entry.getValue()/totalCounts));

Upvotes: 1

Views: 1739

Answers (1)

Tagir Valeev
Tagir Valeev

Reputation: 100219

If you want in-place modification, it's much better to use Map.replaceAll instead of Stream API:

int totalCounts = queryCounts.values().stream()
                             .collect(Collectors.summingInt(Integer::intValue));
queryCounts.replaceAll((k, v) -> v/totalCounts);

However in your case this solution is problematic as division results will be rounded to an int number, thus you almost always will got zeroes in the result. Actually there's the same problem in your code. You probably want to have Map<String, Double> as the resulting type. So you probably need to create a completely new Map:

Map<String, Double> averages = queryCounts.entrySet().stream()
                                          .collect(Collectors.toMap(Entry::getKey,
                                              e -> ((double)e.getValue())/totalCounts));

An alternative would be to have queryCounts declared as Map<String, Double> in the first place. This way you can use replaceAll:

double totalCounts = queryCounts.values().stream()
                             .collect(Collectors.summingDouble(Double::doubleValue));
queryCounts.replaceAll((k, v) -> v/totalCounts);

Finally there's also one more alternative which is the most efficient, but dirty. Your code assumes that original (non-averaged) queryCounts are unnecessary after averageCounters() is called. Thus you can keep queryCounts as Map<String, Integer> (which is more effective than counting to Map<String, Double>), but then change the Map values type like this:

double totalCounts = queryCounts.values().stream()
                             .collect(Collectors.summingInt(Integer::intValue));
Map<String, Object> map = (Map<String, Object>)queryCounts;
map.replaceAll((k, v) -> ((Integer)v)/totalCounts);
Map<String, Double> averages = (Map<String, Double>)map;
queryCounts = null;

The similar trick is performed in JDK inside the Collectors.groupingBy implementation.

Upvotes: 9

Related Questions