reduce method in Java 8 streams

  • Last Updated: March 30, 2024
  • By: javahandson
  • Series
img

reduce method in Java 8 streams

In this article, we will try to understand what is a reduce method in Java 8 streams. We will also learn about different variants of reduce methods with examples.

 

Java 8 Stream API has given a set of reduce methods that perform the reduction operations. These reduction operations are terminal operations that combine the elements of a stream to produce something concrete or a single value.

Ex. We can use a reduce operation to calculate the sum of all the numbers in a Stream or we can use a reduce operation to find the maximum or minimum number in a Stream or we can also use it to concatenate the strings and so on etc.

So in short reduction operation helps to reduce a Stream into a single value. This single value can be something concrete like an Integer a String a List etc.

How do reduce work?

Say we have a list of numbers. Without using the reduce operation how can we find the sum of all the numbers? Please check the code below.

package com.javahandson.reduce;

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

public class NoReduce {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        int sum = 0;
        for (int number : numbers) {
            sum = sum + number;
        }
        System.out.println("Sum of all the numbers : "+sum);
    }
}
Output: Sum of all the numbers : 15

In the above example, we have initialized a sum variable to 0 and then we have iterated over each element of the list and reduced those list of numbers into a single result by repeatedly using the addition operation. Here we have manually written the code to iterate over the numbers and then perform the addition operation.

In the case of Stream API, we have a reduce method that helps us to iterate over the stream of elements and perform an operation on those elements. The reduce method takes in 2 arguments i.e.

a. An initial value of 0.

b. A BinaryOperator to combine 2 elements and produce a new value. We can use the below lambda expression to perform the addition operation.

(a, b) -> a + b

package com.javahandson.reduce;

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

public class Summing {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        int sum = numbers.stream()
                .reduce(0, (a, b) -> a + b);
        System.out.println("Sum of all the numbers : "+sum);
    }
}
Output: Sum of all the numbers : 15

In the above example, the lambda combines each element repeatedly until the stream is reduced to a single value. We will try to understand this better with the below diagram.

reduce in java 8 streams

Firstly 0 is taken as the first parameter of the lambda ( a ) and 1 is consumed from the stream and used as a second parameter ( b ). 0 + 1 produces 1 and 1 becomes the new accumulated value.

Then the lambda is called again with the new accumulated value i.e. 1 and the next element from the stream i.e. 2 which produces the next accumulated value 1 + 2 = 3.

Moving forward the lambda is called again with the new accumulated value i.e. 3 and the next element from the stream i.e. 3 which produces the next accumulated value 3 + 3 = 6 this goes on till it reaches the last element of the stream hence we get the final result as 15.

Types of reduce methods

1. T reduce(T identity, BinaryOperator <T> accumulator)

This reduce operation reduces the stream’s elements using the identity value as an initial value and an accumulator function.

In the above section, we have used the same variant of reduce method to add all the elements in a stream. As we were adding the numbers, we used the initial value as 0. If we were multiplying the numbers then we would have used 1 as an initial value.

Parameters:

identity - It is a initial value that we use in an accumulator funtion.
accumulator - It represents an operation that operates on two operands of the same type and produces a result of the same type as the operands. 

What is a BinaryOperator?

BinaryOperator is a functional interface that is part of the java.util.function package. It represents an operation that operates on two operands of the same type and produces a result of the same type as the operands. The BinaryOperator interface is a specific case of BiFunction where the operands and the result all share the same type.

Write a program to add 2 numbers using the BinaryOperator interface.

package com.javahandson.reduce;

import java.util.function.BinaryOperator;

public class BinaryOperatorDemo {
    public static void main(String[] args) {

        BinaryOperator<Integer> binaryOperator = (a, b) -> a + b;
        int sum = binaryOperator.apply(5, 10);
        System.out.println("Sum of the 2 numbers is : "+sum);
    }
}
Output: Sum of the 2 numbers is : 15

So the lambda expression is the apply method that we are implementing and when we call the apply method the lambda expression gets executed and it performs the addition.

In the case of the reduce method, we don’t have to call the apply method explicitly. It gets called internally and adds up all the numbers in the stream one by one.

2. Optional <T> reduce(BinaryOperator <T> accumulator)

This reduce operation combines the elements of the stream using the provided accumulator function. This method does not require an identity value because it returns an Optional which means the result can be empty if the stream is empty, thus safely handling the reduction of empty streams.

Write a program to sum all the elements in a stream.

package com.javahandson.reduce;

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

public class Summing {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        Optional<Integer> optionalSum = numbers.stream().reduce((a, b) -> a + b);
        System.out.println("Sum of all the numbers : "+optionalSum);
    }
}
Sum of all the numbers : Optional[15]

In the above example the result returned is a Optional but we need the result as an integer. The optional interface gives us some inbuilt methods to check if the result is present or not. If the result is present then we can fetch that result using a lambda expression like below.

package com.javahandson.reduce;

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

public class Summing {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        Optional<Integer> optionalSum = numbers.stream().reduce((a, b) -> a + b);
        System.out.println("Optional sum of all the numbers : "+optionalSum);
        optionalSum.ifPresent(sum -> System.out.println("Sum of all the numbers : "+sum));
    }
}
Output: Optional sum of all the numbers : Optional[15]
        Sum of all the numbers : 15

Suppose the stream is empty then the result will be empty and as we are using Optional hence we handle such scenarios in multiple ways like below.

package com.javahandson.reduce;

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

public class Summing {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList();

        // If the result is empty then final result will not be printed
        Optional<Integer> optionalSum = numbers.stream().reduce((a, b) -> a + b);
        System.out.println("Optional sum of all the numbers : "+optionalSum);
        optionalSum.ifPresent(sum -> System.out.println("Sum of all the numbers : "+sum));

        // If the result is empty then we can print some else value
        int sum = optionalSum.orElse(0);
        System.out.println("Printing 0 if the result is empty : "+sum);

        // If the result is empty then we can throw an exception with some message
        int result = optionalSum.orElseThrow(() -> new IllegalStateException("No values to sum."));
        System.out.println("The sum is: " + result);
    }
}
Output:
Optional sum of all the numbers : Optional.empty
Printing 0 if the result is empty : 0
Exception in thread "main" java.lang.IllegalStateException: No values to sum.
	at com.javahandson.reduce.Summing.lambda$main$2(Summing.java:23)
	at java.util.Optional.orElseThrow(Optional.java:290)
	at com.javahandson.reduce.Summing.main(Summing.java:23)

3. <U> reduce(U identity, BiFunction < U,? super T,U > accumulator, BinaryOperator <U> combiner)

This reduce operation is designed to be used with parallel streams. The accumulator function performs the reduction operation and the combiner function combines the partial results of the accumulator function to ensure correct results in parallel executions.

The above lines are a bit difficult to understand let us try to understand it in a better way.

Write a program to fetch the even numbers from a list of numbers and store those even numbers in a new list using the reduce method.

package com.javahandson.reduce;

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

public class ReduceUsingCombiner {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        List<Integer> evenNumbers = new ArrayList();
        numbers.stream()
                .filter(number -> number % 2 == 0)
                .forEach(number -> evenNumbers.add(number));

        System.out.println(evenNumbers);
    }
}
Output: [2, 4, 6, 8]

In the above example, we are updating an evenNumbers list that is a global variable. Here we are performing a shared mutability. This is too bad and we should not be doing this, the above logic breaks if it is a parallel stream.

package com.javahandson.reduce;

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

public class ReduceUsingCombiner {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        List<Integer> evenNumbers = new ArrayList();
        numbers.parallelStream()
                .filter(number -> number % 2 == 0)
                .forEach(number -> evenNumbers.add(number));

        System.out.println(evenNumbers);
    }
}
Output: [6, 4, 2, 8]

The above code will execute several times and it will work fine. But one fine day you will see one of the numbers is missing.

Output: [6, 2, 8]

So what happened to 4? It seems we had a race condition and one object got dropped in the mix and this becomes very hard to debug.

So what is the better way to write this code? What we want to avoid is shared mutability. Mutability is OK but shared mutability is evil. Hence we will not update the global variable in the below example.

package com.javahandson.reduce;

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

public class ReduceUsingCombiner {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);

        List<Integer> filteredEvenNumbers = numbers.parallelStream()
                .filter(number -> number % 2 == 0)
                // Using reduce to accumulate into a new list, avoiding mutation.
                .reduce(new ArrayList<Integer>(),
                        (List<Integer> list, Integer number) -> {
                    // Instead of mutating the list, create a new list and add all elements.
                    List<Integer> newList = new ArrayList<>(list);
                    newList.add(number);
                    return newList;
                },
                        (List<Integer> list1, List<Integer> list2) -> {
                    // For combining two lists, create a new list and add all from both.
                    List<Integer> newList = new ArrayList<>(list1);
                    newList.addAll(list2);
                    return newList;
                });

        System.out.println(filteredEvenNumbers);
    }
}
Output: [2, 4, 6, 8]

In the above example:

  • We first take an identity variable i.e. a new array list
  • We have written an accumulator function in which instead of mutating the global list we create a new list and add all the elements to it. In this accumulator function, we are not changing the shared global variable hence to the outside world it doesn’t matter and this function can be executed concurrently.
  • In the combiner function, we are collecting the sub-objects together when the code is executed in parallel. Collecting little collections into bigger collections.

The above reduce variant with the combiner function is very difficult to understand and code. This code is complex as well as error-prone. So what if instead of us doing the above code Java does it for us? Yes, you have heard it right. We don’t have to write such a difficult piece of code. Java has introduced the Collectors package to do such complex stuff for us. Collectors package Collect the data properly, thread-safe, and handle the concurrency problem. Hence the problem is solved for us.

Write a program to fetch the even numbers from a list of numbers and store those even numbers in a new list using Collectors API.

package com.javahandson.reduce;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class CollectorsExample {
    public static void main(String[] args) {
        
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        List<Integer> filteredEvenNumbers = numbers.parallelStream()
                .filter(number -> number % 2 == 0)
                .collect(Collectors.toList());
        System.out.println(filteredEvenNumbers);
    }
}
Output: [2, 4, 6, 8]

Collectors API is altogether a big concept and we will learn about the Collectors API in depth in a different article.

In the below section, we will try to explore a couple of use cases that are used frequently.

Maximum Number in a stream

Write a program to find the maximum of numbers in a stream.

package com.javahandson.reduce;

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

public class MaxNumber {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(4, 2, 5, 1, 3);

        Optional<Integer> maxNumber = numbers.stream().reduce((a, b) -> a > b ? a : b);
        maxNumber.ifPresent(number -> System.out.println("Max number is : "+number));

        // We can also make use of method reference
        Optional<Integer> maxNumberMethodRef = numbers.stream().reduce(Integer::max);
        maxNumberMethodRef.ifPresent(number -> System.out.println("Max number using method ref is : "+number));
    }
}
Output: Max number is : 5
Max number using method ref is : 5

Minimum Number in a stream

Write a program to find the minimum of numbers in a stream.

package com.javahandson.reduce;

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

public class MinNumber {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(4, 2, 5, 1, 3);

        Optional<Integer> minNumber = numbers.stream().reduce((a, b) -> a < b ? a : b);
        minNumber.ifPresent(number -> System.out.println("Min number is : "+number));

        // We can also make use of method reference
        Optional<Integer> minNumberMethodRef = numbers.stream().reduce(Integer::min);
        minNumberMethodRef.ifPresent(number -> System.out.println("Min number using method ref is : "+number));
    }
}
Output: Min number is : 1
Min number using method ref is : 1

So this is all about the reduce method in Java 8 streams with examples. If you have any questions on this topic please raise them in the comments section. If you liked this article then please share this post with your friends and colleagues so that they can learn about Java and its frameworks in more depth.

Leave a Comment