Thursday, January 5, 2023

Reduction Operations in Java Stream API

Stream API contains many terminal operations (such as average, sum, min, max, and count) that return one value by combining the contents of a stream. These operations are called reduction operations in Java Stream API because these operations reduce the stream to a single non-stream value.

Examples of these reduction operations can be seen in the post Java Stream API Examples.

Apart from these above mentioned reduction operations, Java Stream API also provides general-purpose reduce methods that can be used with any user supplied logic to get a value from the stream.

Stream API in Java provides three versions of reduce() method-

  1. Optional<T> reduce(BinaryOperator<T> accumulator)- This reduce method returns an object of type Optional which contains the result. Notice that the result stored by Optional is of type T which also happens to be the element type of the stream.

    BinaryOperator is a functional interface which means it will have a single Abstract Method. Thus, accumulator is a function that will implement the method of the interface.
    If you see the description for BinaryOperator it says- Represents an operation upon two operands of the same type, producing a result of the same type as the operands.

    BinaryOperator extends another functional interface BiFunction which has this method-

    • R apply(T t, U u)- Applies this function to the given arguments.
    • Where R is the type of the result, T is the type of the first argument and U is the type of the second argument. (This explanation will help with the third form of the reduce method so please bear with me!).
    But in case of Binary Operator, as we have already seen in the explanation, two operands as well as the result are of same type so apply method effectively becomes T apply(T t, T u) in context of BinaryOperator.

    Here two things to note are-

    • When reduction is performed on the elements of this stream, using accumulation function (which is actually this apply method T apply(T t, Tu)) t will contain the previous result and u will contain the next element.
    • In the first invocation of this form of reduce method t will contain the first element.
  2. T reduce(T identity, BinaryOperator<T> accumulator)- This reduce method returns the result of type T which is same as the element type of the stream. The provided identity value must be an identity for the accumulator function. This means that for all t, accumulator.apply(identity, t) is equal to t i.e. applying the accumulation operation on the identity value and any element of the stream will give you back the element.

    For example, if the operation is addition then the identity value will be 0 as 0 + element = element, in case operation is multiplication then identity value is 1 as 1 * element = element.

  3. <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)- In the third form you can see there are three parameters where apart from identity and accumulator (which is a BiFunction, as explained above) function there is also a combiner function which is a BinaryOperator. Combiner function gives user a way to tell how partial results are to be combined. It becomes important in case parallel stream is used (we’ll see an example soon).

    Another thing you should have noticed is the return type, here it is different from the element type of the stream. In other two variants of the reduce method return type is either an object of Optional (where value stored in the Optional object is same as element type of the stream) or same as element type of the stream.

Java Stream API reduce method example

Let’s see some examples of the reduce method in Java Streams. For that lets take an Employee class where employee with maximum salary is needed as result. Using the first two variants it can be done as-

Employee class

public class Employee {
    private String lastName;
    private String firstName;
    private String empId;
    private int age;
    private int salary;
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getEmpId() {
        return empId;
    }
    public void setEmpId(String empId) {
        this.empId = empId;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
    public int getSalary() {
        return salary;
    }
    public void setSalary(int salary) {
        this.salary = salary;
    }
}

Class with reduce methods

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class ReduceDemo {

    public static void main(String[] args) {       
        List<Employee> empList = createList();
        
        // Using reduce method which returns Optional object
        Optional<Employee> result = empList.stream().reduce((e1, e2) -> 
          e1.getSalary() > e2.getSalary() ? e1 : e2);
        if(result.isPresent()){
            System.out.println("Employee with max salary - " + result.get().getFirstName() 
             + " salary " + result.get().getSalary());
        }
        
        // Using reduce method with identity element
        Employee emp = empList.stream().reduce(new Employee(), (e1, e2) -> 
          e1.getSalary() > e2.getSalary() ? e1 : e2);
        System.out.println("Employee with max salary - " + emp.getFirstName() 
          + " salary " + emp.getSalary());
        
    }
    
    // /Stub method to create list of employee objects
    private static List createList(){
        List<Employee> empList = new ArrayList<Employee>();
        Employee emp = new Employee();
        emp.setEmpId("E001");
        emp.setAge(40);
        emp.setFirstName("Ram");
        emp.setLastName("Chandra");
        emp.setSalary(5000);
        empList.add(emp);
        emp = new Employee();
        emp.setEmpId("E002");
        emp.setAge(35);
        emp.setFirstName("Sheila");
        emp.setLastName("Baijal");
        emp.setSalary(7000);
        empList.add(emp);
        emp = new Employee();
        emp.setEmpId("E003");
        emp.setAge(24);
        emp.setFirstName("Mukesh");
        emp.setLastName("Rishi");
        emp.setSalary(9000);
        empList.add(emp);    
        return empList;
    }
}

Output

Employee with max salary - Mukesh salary 9000
Employee with max salary - Mukesh salary 9000

Reduce method with combiner example

Here let’s see why combiner is important. If there is a list of numbers and you want to get the product of square root of all the numbers then using reduce method, where no combiner is specified, it can be done as-

public class ReduceDemo1 {
    public static void main(String[] args) {        
        List<Double> numList = Arrays.asList(9.0, 4.0, 25.0);        
        double productOfSqrRoots = numList.parallelStream().reduce(1.0, (a, b) -> a * Math.sqrt(b));
        System.out.println("" + productOfSqrRoots);                
    }
}

This will give result as 6.344227580643384 which is not correct. It is happening because not specifying a combiner means accumulator function itself will be used as combiner function too. In that case when partial results are combined the square root is done again resulting in wrong value.

Correct way will be to define a combiner function which will just multiply the partial results.

public class ReduceDemo1 {
    public static void main(String[] args) {
        List<Double> numList = Arrays.asList(9.0, 4.0, 25.0);
        double productOfSqrRoots = numList.parallelStream().reduce(1.0, (a, b) -> 
          a * Math.sqrt(b), (a,b)->a*b);
        System.out.println("" + productOfSqrRoots);        
    }
}

Output

30.0

Here note that this problem will only happen when parallel stream is used if you are using normal stream then there won’t be any partial results to combine.

public class ReduceDemo1 {
    public static void main(String[] args) {        
        List<Double> numList = Arrays.asList(9.0, 4.0, 25.0);        
        double productOfSqrRoots = numList.stream().reduce(1.0, (a, b) -> a * Math.sqrt(b));
        System.out.println("" + productOfSqrRoots);        
    }
}

Since sequential stream is used here rather than parallelStream so you will get a correct output 30.0 now.

Sum using reduce method

Using the Employee class as used above if you want the sum of all the salaries you can do that using reduce method.

int salarySum = empList.stream().reduce(0, (sum, e) -> sum + e.getSalary(), Integer::sum);
System.out.println("Sum of all salaries " + salarySum); 

Though the explicit map-reduce form is more readable and therefore should usually be preferred. Using a chain of map and reduce to do the same thing-

int salarySum1 = empList.stream().mapToInt(Employee::getSalary).reduce(0, (s1, s2) -> s1+s2);
System.out.println("Sum of all salaries " + salarySum1);

This looks more readable, you are first getting the salary of all the employees using the map method and then using reduce method summing them.

That's all for this topic Reduction Operations in Java Stream API. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Java Advanced Tutorial Page


Related Topics

  1. Java Stream API Tutorial
  2. collect() Method And Collectors Class in Java Stream API
  3. Spliterator in Java
  4. Interface Static Methods in Java
  5. Java Stream API Interview Questions And Answers

You may also like-

  1. Effectively Final in Java 8
  2. Covariant Return Type in Java
  3. Why wait(), notify() And notifyAll() Methods Are in Object Class And Not in Thread Class
  4. Blocking Methods in Java Concurrency
  5. Java ArrayBlockingQueue With Examples
  6. How to Sort an ArrayList in Descending Order in Java
  7. How HashMap Works Internally in Java
  8. Difference Between Checked And Unchecked Exceptions in Java

No comments:

Post a Comment