Java Stream Reduce

Java Stream API provides several methods, including reduction and terminal functions, which also support parallelization. In this post, we will cover the Java stream reduce method and its different operations.

Java Stream Reduce

Java stream API provides one method to perform reduction operations on its values. A reduction operation produces one final value from the stream values. <em><strong>Stream.reduce()</strong></em> is the method used for reduction. We can use this method with sequential and parallel streams and it has three variants:

  • With one parameter called an accumulator.
  • Two parameters identity and accumulator.
  • Three parameters identity, accumulator and combiner.

In this post, I will show you different examples of stream reduce() with sequential and parallel streams.If you want to put it in a simple way, Java stream reduce combine elements of a stream and produce a simple value. Before getting in to more technical details, let’s take a simple sum operation to understand how the stream reduce works:

Traditional Way (before Java 8)

int[] numbs = {1,3,5,7,9,11};
int total = 0;
for (int i: numbs) {
  total += i;
}

System.out.println("total is : " + total); //total as 36

With java 8 stream reduce, we can perform the same operation as:

int[] numbs = {1,3,5,7,9,11};
int sum = Arrays.stream(numbers).reduce(0, (a, b) ->a + b);
System.out.printf("Total is " + sum);  //total as 36

//other alternate using method refrence
sum = Arrays.stream(numbers).reduce(0, Integer::sum);

1. Method Signature

Here is a method signature from the reduce method from the Stream interface

T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
  1. identity – initial value
  2. accumulatorfunctional interface which accepts 2 values and produce new value

2. Accumulator, Identity and combiner:

The main concepts of Stream reduce are accumulator, identity and combiner. Let me give you a quick introduction to each of them before moving into details:

  • Accumulator: This is a function with two parameters. This function is used to do the reduction operation on a stream. The first parameter is the result till the current execution, and the second parameter is the current value of the stream.
  • Identity: If the stream is empty, we use it as the result. Else use it as the initial value of the reduction.
  • Combiner: Cit mainly uses Combiner for parallel execution. Combiner combines the results of all sub-streams that run parallel.

3. Java stream reduce example with accumulator:

Let’s start with a simple example. Suppose we want to find out the average of all numbers of a list. For that:

  1. First, we need to find out the sum of all the numbers in that list.
  2. Divide that sum by the total count.

The first thing that comes to our mind to solve this problem is by using a loop. We can iterate through the list elements one by one to find out the sum and then  find out the average by dividing.

import java.util.Arrays;
import java.util.List;

public class StreamReduceExample {

    public static void main(String[] args) {
        List < Integer > givenNumbers = Arrays.asList(2, 4, 6, 8, 10);
        int sum = 0;
        for (int i = 0; i < givenNumbers.size(); i++) {
            sum += givenNumbers.get(i);
        }
        System.out.println("Average " + sum / givenNumbers.size());
    }
}

We can use stream reduce and rewrite the above program as like below:

import java.util.Arrays;
import java.util.List;

public class StreamReduceExample {

	public static void main(String[] args) {

		List<Integer> givenNumbers = Arrays.asList(2, 4, 6, 8, 10);
		int average = givenNumbers.stream().reduce((total, currentValue) -> total + currentValue).orElse(0) / givenNumbers.size();
		System.out.print("Average : " + average);
	}
}

Here, we are calculating the average of numbers in a list. ‘givenNumbers’ is a list of six numbers. We are finding out the average of these numbers and storing that value on variable ‘average’. You can see that only one line required to calculate the average and no extra variable is required to store the total ‘sum’ that we did in the ‘for loop’ example.

(total, currentValue) -> total + currentValue lambda is the accumulator we are using in this example. This accumulator will add all elements of the integer stream. You can also replace this accumulator with method reference Integer::sum.

The return value is Optional.orElse is to safely get the sum. If the value is not present, it will return zero. We are dividing the total sum by the size of the list to get the average. That’s it.

4. Java stream reduce with identity and accumulator:

In the above example, we have seen how to use one accumulator with Java stream reduce method. We can also pass one more value as the first argument of reduce(). We call this identity. Use this initial value before it does the reduction if the stream holds no elements; it returns this identity value. For example:

import java.util.Arrays;
import java.util.List;

public class StreamReduceExample {

    public static void main(String[] args) {
        List < Integer > givenNumbers = Arrays.asList(2, 4, 6, 8, 10);
        int result = givenNumbers.stream().reduce(5, Integer::sum);
        System.out.println(result);
    }
}

This example will print 35, the sum of all list elements and 5.Now, if we change the above list to an empty list as like below:

List < Integer > givenNumbers = Collections.emptyList();
int result = givenNumbers.stream().reduce(5, Integer::sum);

It will print 5 i.e. the identity value. The return value is not optional if you pass an identity value. So, it is always a good practice to include identity with stream reduce operation.

5. Java stream reduce example with identity, accumulator and combiner:

One more variant of Java stream reduce is available. It takes one third parameter called combiner. The first parameter is the identity, and the second one is the accumulator. The combiner is like an accumulator. It is used to combine two values and it should be compatible with the function used in the accumulator. Combiner is useful only in case of parallel streams. For parallel streams, combiner is used to combine the result of all sub-streams. For example:

import java.util.Arrays;
import java.util.List;

public class StreamReduceExample {

    public static void main(String[] args) {

        List < String > numbers = Arrays.asList("one", "two", "three", "four", "five");
        String result = numbers.parallelStream().reduce(
" ", (first, second) - > first + second, String::concat);
        System.out.println(result);
    }
}

In this example, we are using one parallel stream.Combiner is also useful in sequential stream. Let’s consider the below example:

List < String > numbers = Arrays.asList("one", "two", "three", "four", "five");
int sum = numbers.stream()
                 .reduce(0, (first, second) - > first + second.length(), Integer::sum);

System.out.println(sum);

Here, we are finding out the length of all strings in the list. If we don’t use the combiner, it will throw one error. The identity is an integer, but the stream is a list of strings. The compiler expects the result and the identity to be a string, not an integer. The combiner tells the compiler that the result is an integer sum. You can use a combiner in a sequential stream, but they are useful only in a parallel stream.

6. Using different accumulator and combiner:

Let’s consider the below example program:

import java.util.Arrays;
import java.util.List;

public class StreamReduceExample {

    public static void main(String[] args) {
        List < String > numbers = Arrays.asList("one", "two", "three", "four", "five");
        String result = numbers.stream()
                               .reduce("=>", (first, second) - > first + "$" + second, 
                                      (first, second) - > first + "%" + second);
        System.out.println(result);
    }
}

In this example, we are using two different accumulators and combiner. The accumulator adds one ‘$’ between the result and the current string and the combiner adds one ‘%’. If you execute this program, it will print the below output:

=>onetwothreefour$five

i.e. the combiner is not used. This is because it uses the combiner only with a parallel stream, as I have explained above. If you change the ‘stream()’ method to ‘parallelStream()’, it will print ‘=>two%=>four%=>$five’.

7. Java stream reduce example with objects:

We have seen different examples of stream reduce on how to use it with string, integers and other values. Similarly, we can also use it with any custom objects. The below example shows you how we can use stream reduce to find different values in a stream of objects:

import java.util.Arrays;
import java.util.List;
class Expense {
    String category;
    private double expense;
    Expense(String _category, double _expense) {
        this.category = _category;
        this.expense = _expense;
    }
    double getExpense() {
        return expense;
    }
    String getCategory() {
        return category;
    }
}

public class ExpenseTracker {
    public static void main(String[] args) {
        List < Expense > expenseList = Arrays.asList(new Expense("fuel", 200),
            new Expense("grocery", 1000),
            new Expense("rent", 500),
            new Expense("outdoor", 560.5));
            
        double totalExpense = expenseList.stream()
            .reduce(0.0, (result, current) - > result + current.getExpense(), Double::sum);
        Expense highestExpense = expenseList.stream()
            .reduce((result, current) - > result.getExpense() > current.getExpense() ? result : current)
            .orElse(null);
            
        double lowestExpenseAmount = expenseList.stream()
            .map(Expense::getExpense)
            .reduce(Double.MAX_VALUE, (result, current) - > result < current ? result : current);

        System.out.println("Total expense : " + totalExpense);
        System.out.println("Highest expense category : " + highestExpense.getCategory() + " expense : " + highestExpense.getExpense());
        System.out.println("Lowest expense : " + lowestExpenseAmount);
    }
}

Explanation:

  • In this example, we are storing the expense report of a user in a list and we are calculating different values of that list using stream reduce.
  • The class ‘Expense’ is used to store the expense. It has two variables: one String variable ‘category’ to hold the expense category and double variable ‘expense’ to hold the expense amount. The constructor of this class takes these two variables and assigns them.
  • ExpenseTracker class is the main class of this program.
  • Here, we have one list of Expense objects called expenseList. We have four different objects in this list with a different category and expense amount.
  • We are calculating three different values from this list: the total expense amount, the Expense object with the highest amount of expense and the smallest expense amount.
  • totalExpense variable holds the total amount of expense. We are creating one stream on the expenseList and using reduce with identity 0.0, accumulator to find the sum of all expenses and combiner as Double::sum.
  • highestExpense is an Expense object. This reduce operation finds out the Expense object with the highest amount of expense. We are using one ternary conditional operator to find the highest expense object.
  • lowestExpenseAmount variable holds the lowest expense of these objects. Here, we are using map before reduce. The advantage of map is that it returns one stream. The map operation returns one stream of expenses. Then we are using reduce to find out the lowest expense amount. The identity is Double.MAX_VALUE, and the accumulator is using one ternary operation to return the lowest expense amount.

8. Reduce with a parallel stream:

Parallel stream operations are faster than sequential. If you have a large set of data, you can use parallelism to get a boost on the application performance. Previously, we have seen how to use stream reduce with a parallel stream. In this example, we will try with a large set of data and calculate the approximate time to do one reduce operation in sequential and parallel stream.

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class ParallelExample {

    public static void main(String[] args) {
        Random random = new Random();
        final List < Integer > LARGE_LIST = new ArrayList < > (100000000);

        for (int i = 0; i < 100000000; i++)
            LARGE_LIST.add(random.nextInt(10000));

        Instant startTime = Instant.now();
        int largestNumber = LARGE_LIST.stream()
            .reduce(0, (result, current) - > result > current ? result : current);
        long totalTime = Duration.between(startTime, Instant.now()).toMillis();

        Instant parallelStartTime = Instant.now();
        int largestNumberParallel = LARGE_LIST.parallelStream()
            .reduce(0, (result, current) - > result > current ? result : current, Integer::max);

        long totalTimeParallel = Duration.between(parallelStartTime, Instant.now()).toMillis();

        System.out.println("Largest number sequential : " + largestNumber + " time : " + totalTime);
        System.out.println("Largest number parallel : " + largestNumberParallel + " time : " + totalTimeParallel);
    }
}
  1. We are using one large set of data: 100000000 integer values. All values are random.
  2. We calculate the largest value of this list using one sequential stream and one parallel stream. We are recording the time before and after the calculation.
  3. largestNumber and totalTime variables hold the largest number and total time for the sequential reduce operation. Similarly, largestNumberParallel and totalTimeParallel are for parallel stream reduce operation.

If you run it, it will produce one output like below :

Largest number sequential: 9999 time: 577
Largest number parallel: 9999 time: 149

Note that the output may vary on each execution and on different systems. In my system, the parallel execution makes the process almost four times faster!

8. Additional Examples

Now we have a good understanding of the stream reduce method, let’s take a look at some of the common examples:

8.1. Math Operations

With the reduce method, we can easily work on the following operations

  1. Addition
  2. Division
  3. Multiple
  4. Subtractions
int[] numbs = {1, 3, 5, 7, 9, 11, 13, 21, 45, 23};

int total = Arrays.stream(numbs).reduce(0, (a,b) ->a + b);
int sum   = Arrays.stream(numbs).reduce(0, Integer::sum);

total =     Arrays.stream(numbs).reduce(0, (a,b) ->a - b); //-138
total =     Arrays.stream(numbs).reduce(0, (a,b) ->a * b); //0
total =     Arrays.stream(numbs).reduce(0, (a,b) ->a / b); //0

8.2. Finding Min and Max using reduce Method

We can also use Java stream reduce method to find min and max for a given input.

int[] numbs = {1, 3, 5, 7, 9, 11, 13, 21, 45, 23};

int max       = Arrays.stream(numbs).reduce(0, (a,b) ->a > b ? a : b); //45
int maximum   = Arrays.stream(numbs).reduce(0, Integer::max);  //45

int min       = Arrays.stream(numbs).reduce(0, (a,b) ->a > b ? a : b); //1
int minimum   = Arrays.stream(numbs).reduce(0, (a,b) ->a > b ? a : b); //1

You can also use the reduce method in combination with Java 8 Map reduce feature

Summary:

In this post, we looked at the Java stream reduce method. Stream reduce() is an important Java stream API method. We have learnt how to use reduce() with sequential and parallel streams with different use cases. Using this method with parallel stream can reduce the execution time, and also it reduces the boilerplate code. The source code for this article is available on our GitHub repository