The Sum of all Reductions

Donald Raab
Javarevisited
Published in
5 min readFeb 2, 2018

The reduction of all sums

Belmar, NJ during the Deep Freeze of 2018

During the Deep Freeze of 2018, everything in the world seemed to be reduced to ice and snow, including the sky. Even the sun seemed to be reduced as it ran away from my camera with a shiver.

This got me thinking about different kinds of reductions we have available in Java with Eclipse Collections and Java Streams. I wondered how many ways we could define sum with the various methods available.

Summing an array of ints

Let’s consider several ways we can sum the values in an int array in Java.

Here is the data we will use.

int[] ints = {1, 2, 3, 4, 5};

// This supplier will generate an IntStream on demand
IntStream intStream = Arrays.stream(ints);

// This creates an IntList from Eclipse Collections
IntList intList = IntLists.immutable.with(ints);

For Loop

int sumForLoop = 0;
for (int i = 0; i < ints.length; i++)
{
sumForLoop += ints[i];
}
Assertions.assertEquals(15, sumForLoop);

forEach (IntStream) / each (IntList)

// sumForEach will be effectively final
int[] sumForEach = {0};
intStream.forEach(e -> sumForEach[0] += e);

Assertions.assertEquals(15, sumForEach[0]);

// sumEach will be effectively final
int[] sumEach = {0};
intList.each(e -> sumEach[0] += e);

Assertions.assertEquals(15, sumEach[0]);

injectInto (IntList)

// injectInto boxes on IntList as there is no primitive version
int sumInject =
intList.injectInto(Integer.valueOf(0), Integer::sum).intValue();

Assert.assertEquals(15, sumInject);

reduce (IntStream)

// reduce does not box on IntStream
int sumReduce =
intStream.reduce(Integer::sum).getAsInt();

Assertions.assertEquals(15, sumReduce);

sum (IntStream / IntList)

int sum1 = intStream.sum();

Assertions .assertEquals(15, sum1);

long sum2 = intList.sum();

Assertions.assertEquals(15, sum2);

Clearly, the sum methods available on IntStream and IntList are the simplest solutions. The minor difference with IntList is that the result is widened to a long which means you can add very large int values without overflowing.

Summarizing an array of ints

When we summarize using the IntSummaryStatistics class that was added in Java 8, we get the count, sum, min, max and average calculated at the same time. This saves you from iterating multiple times. We will use the same data as before.

For Loop

IntSummaryStatistics statsForLoop = new IntSummaryStatistics();
for (int i = 0; i < ints.length; i++)
{
statsForLoop.accept(ints[i]);
}

Assertions.assertEquals(15, statsForLoop.getSum());
Assertions.assertEquals(1, statsForLoop.getMin());
Assertions.assertEquals(5, statsForLoop.getMax());

forEach (IntStream) / each (IntList)

IntSummaryStatistics statsForEach = new IntSummaryStatistics();
intStream.forEach(statsForEach::accept);

Assertions.assertEquals(15, statsForEach.getSum());
Assertions.assertEquals(1, statsForEach.getMin());
Assertions.assertEquals(5, statsForEach.getMax());

IntSummaryStatistics statsEach = new IntSummaryStatistics();
intList.each(statsEach::accept);

Assertions.assertEquals(15, statsEach.getSum());
Assertions.assertEquals(1, statsEach.getMin());
Assertions.assertEquals(5, statsEach.getMax());

injectInto (IntList)

IntSummaryStatistics statsInject =
intList.injectInto(
new IntSummaryStatistics(),
(iss, each) -> {iss.accept(each); return iss;});

Assertions.assertEquals(15, statsInject.getSum());
Assertions.assertEquals(1, statsInject.getMin());
Assertions.assertEquals(5, statsInject.getMax());

collect (IntStream)

IntSummaryStatistics statsCollect =
intStream.collect(
IntSummaryStatistics::new,
IntSummaryStatistics::accept,
IntSummaryStatistics::combine);

Assertions.assertEquals(15, statsCollect.getSum());
Assertions.assertEquals(1, statsCollect.getMin());
Assertions.assertEquals(5, statsCollect.getMax());

Note: I could not use reduce because both parameters have to be the same type. I had to use collect instead, which is a mutable reduction. The collect method on primitive Streams does not take a Collector, but instead takes a Supplier, ObjectIntConsumer (accumulator) and a BiConsumer (combiner).

summaryStatistics (IntStream / IntList)

IntSummaryStatistics stats1 = intStream.summaryStatistics();

Assertions.assertEquals(15, stats1.getSum());
Assertions.assertEquals(1, stats1.getMin());
Assertions.assertEquals(5, stats1.getMax());

IntSummaryStatistics stats2 = intList.summaryStatistics();

Assertions.assertEquals(15, stats2.getSum());
Assertions.assertEquals(1, stats2.getMin());
Assertions.assertEquals(5, stats2.getMax());

Again, the summaryStatistics methods are the simplest solutions.

Summing the lengths of an array of Strings

Let’s say we want to sum the lengths of Strings in an array. This approach could be used for summing any int attribute of an object.

Here is the data we will use.

String[] words = {"The", "Quick", "Brown", "Fox", "jumps", "over", "the", 
"lazy", "dog"};

Stream<String> stream = Stream.of(words);

ImmutableList<String> list = Lists.immutable.with(words);

For Loop

int sumForLoop = 0;
for (int i = 0; i < words.length; i++)
{
sumForLoop += words[i].length();
}

Assertions.assertEquals(35, sumForLoop);

For Each (Stream) / each (ImmutableList)

int[] sumForEach = {0};
stream.forEach(e -> sumForEach[0] += e.length());

Assertions.assertEquals(35, sumForEach[0]);

int[] sumEach = {0};
list.each(e -> sumEach[0] += e.length());

Assertions.assertEquals(35, sumEach[0]);

collectInt (ImmutableList)+ injectInto (IntList)

int sumInject = list
.collectInt(String::length)
.injectInto(Integer.valueOf(0), Integer::sum)
.intValue();

Assertions.assertEquals(35, sumInject);

collect (Stream) + reducing (Collectors)

int sumReducing = 
stream.collect(Collectors.reducing(0,
String::length,
Integer::sum)).intValue();

Assertions.assertEquals(35, sumReduce);

mapToInt (Stream) + Reduce (IntStream)

int sumReduce = stream
.mapToInt(String::length)
.reduce(Integer::sum)
.getAsInt();

Assertions.assertEquals(35, sumReduce);

mapToInt (Stream) + sum (IntStream)

int sum1 = stream
.mapToInt(String::length)
.sum();

Assertions.assertEquals(35, sum1);

collectInt (ImmutableList) + sum (IntList)

long sum2 = list
.collectInt(String::length)
.sum();

Assertions.assertEquals(35, sum2);

collect (Stream) + summingInt (Collectors)

Integer summingInt = stream
.collect(Collectors.summingInt(String::length));

Assertions.assertEquals(35, summingInt.intValue());

sumOfInt (ImmutableList)

long sumOfInt = list.sumOfInt(String::length);

Assertions.assertEquals(35, sumOfInt);

I think in these examples, sumOfInt is the simplest solution.

Summing the lengths of Strings grouped by the first character

In this problem we will group Strings by their first character and sum the length of the Strings for each character. I will prefer to use use primitive maps here for the grouping if possible.

Here is the data.

String[] words = {"The", "Quick", "Brown", "Fox", "jumps", "over", "the", 
"lazy", "dog"};

Stream<String> stream = Stream.of(words).map(String::toLowerCase);

ImmutableList<String> list =
Lists.immutable.with(words).collect(String::toLowerCase);

The Stream and ImmutableList strings are converted to lowercase using map and collect, respectively. We will do this manually in the for loop example.

For Loop

MutableCharIntMap sumByForLoop = new CharIntHashMap();
for (int i = 0; i < words.length; i++)
{
String word = words[i].toLowerCase();
sumByForLoop.addToValue(word.charAt(0), word.length());
}

Assertions.assertEquals(35, sumByForLoop.values().sum());
Assertions.assertEquals(6, sumByForLoop.get('t'));

for Each (Stream) / each (ImmutableList)

MutableCharIntMap sumByForEach = new CharIntHashMap();
stream.forEach(e -> sumByForEach.addToValue(e.charAt(0), e.length()));

Assertions.assertEquals(35, sumByForEach.values().sum());
Assertions.assertEquals(6, sumByForEach.get('t'));

MutableCharIntMap sumByEach = new CharIntHashMap();
list.each(e -> sumByEach.addToValue(e.charAt(0), e.length()));

Assertions.assertEquals(35, sumByEach.values().sum());
Assertions.assertEquals(6, sumByEach.get('t'));

injectInto (ImmutableList)

MutableCharIntMap sumByInject =
list.injectInto(
new CharIntHashMap(),
(map, each) -> {
map.addToValue(each.charAt(0), each.length());
return map;
});

Assertions.assertEquals(35, sumByInject.values().sum());
Assertions.assertEquals(6, sumByInject.get('t'));

reduce (Stream)

MutableCharIntMap sumByReduce = stream
.reduce(
new CharIntHashMap(),
(map, e) -> {
map.addToValue(e.charAt(0), e.length());
return map;
},
(map1, map2) -> {
map1.putAll(map2);
return map1;
});

Assertions.assertEquals(35, sumByReduce.values().sum());
Assertions.assertEquals(6, sumByReduce.get('t'));

aggregateBy (ImmutableList)

ImmutableMap<Character, Long> aggregateBy = list.aggregateBy(
word -> word.charAt(0),
() -> new Long(0),
(sum, each) -> sum + each.length());

Assertions.assertEquals(35,
aggregateBy.valuesView().sumOfLong(Long::longValue));
Assertions.assertEquals(6, aggregateBy.get('t').longValue());

aggregateInPlaceBy (ImmutableList)

ImmutableMap<Character, LongAdder> aggregateInPlaceBy = 
list.aggregateInPlaceBy(
word -> word.charAt(0),
LongAdder::new,
(adder, each) -> adder.add(each.length()));

Assertions.assertEquals(35,
aggregateInPlaceBy.valuesView().sumOfLong(LongAdder::longValue));
Assertions.assertEquals(6, aggregateInPlaceBy.get('t').longValue());

collect (Stream)

MutableCharIntMap sumByCollect = stream.collect(
CharIntHashMap::new,
(map, e) -> map.addToValue(e.charAt(0), e.length()),
CharIntHashMap::putAll);

Assertions.assertEquals(35, sumByCollect.values().sum());
Assertions.assertEquals(6, sumByCollect.get('t'));

collect (Stream) + groupingBy (Collectors) + summingInt (Collectors)

Map<Character, Integer> sumByCollectSummingInt =
stream.collect(Collectors.groupingBy(
word -> word.charAt(0),
Collectors.summingInt(String::length)));
Assertions.assertEquals(35,
sumByCollectSummingInt.values()
.stream().mapToInt(Integer::intValue).sum());
Assertions.assertEquals(Integer.valueO(6), sumByCollectSummingInt.get('t'));

collect (Stream) + sumByInt (Collectors2)

ObjectLongMap<Character> sumByCollectors2 =
stream.collect(
Collectors2.sumByInt(
word -> word.charAt(0), String::length));

Assertions.assertEquals(35, sumByCollectors2.values().sum());
Assertions.assertEquals(6, sumByCollectors2.get('t'));

reduceInPlace (ImmutableList)+ sumByInt (Collectors2)

ObjectLongMap<Character> reduceInPlaceCollectors2 =
list.reduceInPlace(
Collectors2.sumByInt(
e -> e.charAt(0), String::length));

Assertions.assertEquals(35, reduceInPlaceCollectors2.values().sum());
Assertions.assertEquals(6, reduceInPlaceCollectors2.get('t'));

sumByInt (ImmutableList)

ObjectLongMap<Character> sumByInt =
list.sumByInt(e -> e.charAt(0), String::length);

Assertions.assertEquals(35, sumByInt.values().sum());
Assertions.assertEquals(6, sumByInt.get('t'));

The simplest solution here is sumByInt.

Conclusion

We’ve covered a lot of different approaches you can use to sum or summarize values using Java and Eclipse Collections. In the case of summing, using a method with sum in the name will probably give you the simplest solution. You can solve almost any problem using methods like injectInto and reduceInPlace (Eclipse Collections) or collect (Java Stream). Methods like reduce are less useful when your result needs to be different than your input. Methods like aggregateBy and aggregateInPlaceBy give you a more specific result than collect because they always return a Map. Using Collectors2 can be helpful if you want to iterate over a Stream and get a primitive map result easily using collect.

I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.

--

--

Donald Raab
Javarevisited

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.