Reduce Method in Java 8 Streams – identity, accumulator and combiner

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

Reduce Method in Java 8 Streams – identity, accumulator and combiner

The reduce method in Java 8 streams answers one simple question: how do you squeeze a whole stream down into a single value? A sum, a maximum, a longest name, a combined report line. In this article we will build that idea from the ground up, walk through all three overloads, and finish with the traps that catch almost every beginner.

1. Introduction

Most stream pipelines end by handing you a collection. You filter, you map, you collect, and out comes a list. Sometimes, though, a list is not what you want at all. You want one number. One name. One total.

That is where reduction steps in. A reduction walks the stream, keeps a running result, and folds every element into it. When the stream runs dry, the running result is your answer.

Java 8 gives you three reduce methods for this job. They look similar, and beginners mix them up constantly. Once you see what each one adds, the confusion clears up fast.

1.1 What This Article Covers

  • Why a manual loop and a reduce call solve the same problem
  • All three reduce overloads, with the exact signature of each
  • The meaning of identity, accumulator and combiner
  • Everyday reductions: sum, product, max, min, longest string, object merging
  • reduce on IntStream, LongStream and DoubleStream
  • How reduce differs from collect, and when to pick each one
  • Why a careless reduce gives a different answer on a parallel stream
  • Seven mistakes worth avoiding, plus a small end-to-end program

1.2 What a Reduction Really Is

Think about totalling a shopping bill by hand. You start at zero. You add the first item, then the second, then the third. The running total carries forward each time. At the end you read off the last number.

That is a reduction in one sentence. Two things go in, one thing comes out, over and over, until nothing is left.

The Java docs call reduce a terminal operation. Terminal means the pipeline actually runs at that moment and the stream is finished afterwards. You cannot reuse it.

Reductions are everywhere once you look. Sum, product, count, maximum, minimum, string concatenation, boolean AND across a list of flags. Every one of them collapses many values into one.

2. The Problem reduce Solves

Let us start with the plainest example there is, and then swap it for the stream version.

2.1 Summing a List the Old Way

Here is the loop every Java developer has written a hundred times.

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
    }
}

Look closely at what this loop contains. There is a starting value of 0. There is a rule that joins the running total to the next number. And there is the plumbing that walks the list.

The starting value and the rule are the interesting parts. The plumbing is noise you rewrite every single time.

2.2 The Same Job With reduce

Now hand the plumbing to the Stream API and keep only the two interesting bits.

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
    }
}

Two arguments, and that is the whole story:

  • 0 – the starting value, which Java calls the identity
  • (a, b) -> a + b – the rule that joins the running result to the next element, which Java calls the accumulator

Notice what disappeared. No counter, no mutable local variable, no explicit loop. You described the calculation instead of spelling out the steps.

2.3 How the Values Fold Together

The picture below shows the fold happening one element at a time.

reduce method in Java 8 streams folding a list of numbers into a single sum

Follow the steps for [1, 2, 3, 4, 5] with an identity of 0:

  • Java calls the lambda with a = 0 and b = 1. The result 1 becomes the new running value.
  • Next call gets a = 1 and b = 2, so the running value moves to 3.
  • Then a = 3 and b = 3 push it to 6.
  • After that a = 6 and b = 4 give 10.
  • The last call sees a = 10 and b = 5, and 15 comes back to you.

The identity always arrives as the very first a. Every element of the stream takes a turn as b. Keep that pattern in your head and the rest of this article follows easily.

3. The Three reduce Overloads

The Stream interface declares reduce three times. Each version adds one capability the previous one lacked.

3.1 reduce With an Identity

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

This is the version you just saw. Give it a starting value and a rule, and it hands back a plain T.

The return type matters. Because you supplied a starting value, there is always something to return. An empty stream simply gives you the identity back.

List<Integer> empty = new ArrayList<>();

int sum = empty.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum); // Output: 0

// Multiplication needs 1 as the identity, not 0
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int product = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(product); // Output: 120

Swap the identity to match the operation. Zero for addition, one for multiplication, an empty string for concatenation. Get this wrong and your answer quietly shifts.

3.2 reduce Without an Identity

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

Drop the identity and one thing changes: an empty stream now has no answer at all. Java refuses to invent one, so it wraps the result in an Optional.

package com.javahandson.reduce;

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

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

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

        Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
        System.out.println(sum); // Output: Optional[15]

        sum.ifPresent(value -> System.out.println("Sum : " + value)); // Output: Sum : 15
    }
}

The first element becomes the seed here. Java takes it as-is, then folds the rest on top. No made-up starting value creeps into the maths.

An empty stream hands you Optional.empty, and you decide what that means.

List<Integer> numbers = Arrays.asList();
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);

System.out.println(sum); // Output: Optional.empty

// Fall back to a default
System.out.println(sum.orElse(0)); // Output: 0

// Or refuse to continue
int result = sum.orElseThrow(() -> new IllegalStateException("No values to sum."));
// Output: Exception in thread "main" java.lang.IllegalStateException: No values to sum.

So which one do you pick? Use the identity version when zero elements has an obvious answer. Use the Optional version when it does not. The average of nothing is meaningless, and so is the maximum of nothing.

3.3 reduce With a Combiner

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

The third overload looks scary, and its generics do it no favours. It buys you one genuine new power: the result type U no longer has to match the element type T.

Say you have a stream of String names and you want the total number of characters. Elements are strings, the answer is an integer, and the first two overloads cannot bridge that gap.

package com.javahandson.reduce;

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

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

        List<String> names = Arrays.asList("Amit", "Sara", "Vikram", "Neha");

        int totalLetters = names.stream()
                .reduce(0,                                  // identity: an int
                        (total, name) -> total + name.length(),  // accumulator: int + String
                        (left, right) -> left + right);         // combiner: int + int

        System.out.println("Total letters : " + totalLetters); // Output: Total letters : 18
    }
}

Read the three arguments as three separate jobs:

  • identity – the starting result, of type U
  • accumulator – folds one element of type T into a result of type U
  • combiner – merges two partial U results into one

Why does a combiner exist at all? A parallel stream splits the data into chunks and reduces each chunk on its own thread. Each chunk produces a partial result, and something has to stitch those partials back together. That job belongs to the combiner.

3.4 The Three Overloads at a Glance

Overload Returns Empty stream gives Can change type? Typical use
reduce(identity, accumulator) T the identity No Sum, product, boolean AND
reduce(accumulator) Optional<T> Optional.empty No Max, min, longest value
reduce(identity, accumulator, combiner) U the identity Yes Element type differs from result type

Ninety per cent of real code uses the first two. Reach for the third only when the types genuinely differ, and even then check whether a collector reads better.

4. identity, accumulator and combiner

Three words carry all the meaning in reduce. Let us pin each one down properly.

4.1 What Makes a Good Identity

An identity is not just any starting value. It has to be the neutral value for your operation. Feed it to the accumulator alongside any element, and that element must come back untouched.

Write it as a rule and it reads like this: accumulator.apply(identity, x) must equal x for every possible x.

Operation Correct identity Why
Addition 0 0 + x equals x
Multiplication 1 1 * x equals x
String concatenation "" "" + x equals x
Maximum Integer.MIN_VALUE nothing is smaller
Minimum Integer.MAX_VALUE nothing is larger
Boolean AND true true && x equals x

Break this rule and a sequential stream still gives a wrong-but-stable answer. A parallel stream gives a wrong answer that changes between runs, which is far nastier to debug.

4.2 Why the Accumulator Must Be Associative

Associative sounds like exam vocabulary, but the idea is plain. Grouping the operands differently must not change the answer.

Addition passes the test. (1 + 2) + 3 and 1 + (2 + 3) both give 6. Multiplication passes too.

Subtraction fails badly. (10 - 3) - 2 gives 5, while 10 - (3 - 2) gives 9. Division fails for the same reason.

Why should you care? A parallel stream chooses its own grouping. It splits wherever the fork-join framework decides to split, and no two runs have to agree. An associative accumulator makes the grouping irrelevant.

One more requirement rounds this out: the accumulator must be stateless. It should read its two arguments and nothing else. Reaching outside for a counter or a list turns a clean reduction into a race condition.

4.3 When Java Calls the Combiner

Here is a detail that surprises people. On a sequential stream, Java may never call your combiner at all.

List<String> names = Arrays.asList("Amit", "Sara", "Vikram");

// Sequential: the combiner is not needed, so it never runs
int letters = names.stream()
        .reduce(0,
                (total, name) -> total + name.length(),
                (left, right) -> {
                    System.out.println("combiner called");
                    return left + right;
                });

System.out.println(letters); // Output: 14
// Nothing prints from the combiner on a sequential stream

Switch stream() to parallelStream() and the message appears. There is only one partial result in the sequential case, so there is nothing to merge.

This is exactly why a broken combiner can hide for months. Your tests run sequentially, everything passes, and the bug only wakes up the day someone adds .parallel().

4.4 BinaryOperator in One Minute

Both the accumulator and the combiner take a BinaryOperator, so it helps to know what that is.

BinaryOperator<T> lives in java.util.function. It takes two arguments of type T and returns a T. That is the whole interface. It is really just a BiFunction where all three types collapse into one.

package com.javahandson.reduce;

import java.util.function.BinaryOperator;

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

        BinaryOperator<Integer> add = (a, b) -> a + b;
        System.out.println(add.apply(5, 10)); // Output: 15

        // Method references work anywhere a BinaryOperator fits
        BinaryOperator<Integer> max = Integer::max;
        System.out.println(max.apply(5, 10)); // Output: 10
    }
}

When you pass a lambda to reduce, you are implementing apply. Java calls it for you, once per element. You never call it yourself.

If functional interfaces are still fuzzy, our article on predefined functional interfaces walks through the whole family.

5. Everyday Reductions

Theory is done. These are the reductions you will actually write at work.

5.1 Sum and Product

List<Integer> numbers = Arrays.asList(2, 4, 6, 8);

int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println("Sum : " + sum); // Output: Sum : 20

int product = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println("Product : " + product); // Output: Product : 384

Integer::sum reads better than (a, b) -> a + b and does exactly the same work. Our guide to method references in Java 8 covers the four forms in detail.

5.2 Maximum and Minimum

Max and min have no sensible answer for an empty stream, so the Optional overload fits perfectly.

package com.javahandson.reduce;

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

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

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

        Optional<Integer> max = numbers.stream().reduce((a, b) -> a > b ? a : b);
        max.ifPresent(n -> System.out.println("Max : " + n)); // Output: Max : 5

        // The same thing, but easier to read
        Optional<Integer> maxRef = numbers.stream().reduce(Integer::max);
        System.out.println(maxRef.orElse(-1)); // Output: 5

        Optional<Integer> min = numbers.stream().reduce(Integer::min);
        System.out.println(min.orElse(-1)); // Output: 1
    }
}

Streams already ship max(Comparator) and min(Comparator), and those read better still. Use reduce here when your comparison logic does not fit a comparator neatly.

5.3 The Longest Word

Reductions are not restricted to numbers. Any rule that turns two values into one will do.

List<String> words = Arrays.asList("stream", "reduce", "accumulator", "identity");

Optional<String> longest = words.stream()
        .reduce((a, b) -> a.length() >= b.length() ? a : b);

System.out.println(longest.orElse("none")); // Output: accumulator

Notice the >= rather than >. That tiny choice decides which word wins a tie: the earlier one here, the later one if you drop the equals sign.

5.4 Joining Text

You can join strings with reduce, and plenty of tutorials show it.

List<String> words = Arrays.asList("Java", "8", "reduce");

// Works, but creates a brand-new String on every single step
String joined = words.stream().reduce("", (a, b) -> a + " " + b);
System.out.println(joined.trim()); // Output: Java 8 reduce

// Better: one buffer, no throwaway objects
String better = words.stream().collect(Collectors.joining(" "));
System.out.println(better); // Output: Java 8 reduce

Strings in Java are immutable. Every a + " " + b allocates a fresh string and copies both halves into it. For ten words nobody notices. For a hundred thousand rows the cost grows with the square of the input, and your log-formatting job crawls.

Prefer Collectors.joining for text. It keeps one buffer and appends into it.

5.5 Reducing Objects, Not Numbers

Sometimes the thing you want back is a domain object. Merging shopping carts is a nice example.

package com.javahandson.reduce;

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

class Cart {
    final String owner;
    final int items;

    Cart(String owner, int items) {
        this.owner = owner;
        this.items = items;
    }

    // Returns a NEW cart. Nothing is mutated, so this is safe in parallel.
    Cart merge(Cart other) {
        return new Cart(this.owner + "+" + other.owner, this.items + other.items);
    }
}

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

        List<Cart> carts = Arrays.asList(
                new Cart("Amit", 3),
                new Cart("Sara", 5),
                new Cart("Neha", 2));

        Optional<Cart> merged = carts.stream().reduce(Cart::merge);

        merged.ifPresent(c -> System.out.println(c.owner + " : " + c.items));
        // Output: Amit+Sara+Neha : 10
    }
}

The key detail sits inside merge. It builds a new Cart instead of editing either input. That single habit is what keeps a reduction correct when threads get involved.

6. reduce on Primitive Streams

IntStream, LongStream and DoubleStream each declare their own reduce. The shape matches what you already know, minus the boxing.

6.1 IntStream.reduce

int reduce(int identity, IntBinaryOperator op)
OptionalInt reduce(IntBinaryOperator op)

Same two shapes as before. The wrapper changes from Optional<Integer> to OptionalInt, and no Integer objects get created along the way.

package com.javahandson.reduce;

import java.util.OptionalInt;
import java.util.stream.IntStream;

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

        int sum = IntStream.rangeClosed(1, 5).reduce(0, Integer::sum);
        System.out.println("Sum : " + sum); // Output: Sum : 15

        OptionalInt max = IntStream.of(4, 2, 5, 1).reduce(Integer::max);
        System.out.println(max.getAsInt()); // Output: 5

        // 5! computed as a reduction
        int factorial = IntStream.rangeClosed(1, 5).reduce(1, (a, b) -> a * b);
        System.out.println("5! = " + factorial); // Output: 5! = 120
    }
}

There is no three-argument overload here. A primitive stream never changes its element type mid-flight, so a combiner would have nothing extra to do.

6.2 The Ready-Made Shortcuts

Before you write reduce on a primitive stream, check whether the answer already has a name.

  • sum() returns an int, long or double directly
  • max() and min() return OptionalInt, OptionalLong or OptionalDouble
  • average() always returns an OptionalDouble
  • count() returns a long and needs no reduction at all
  • summaryStatistics() hands you count, sum, min, max and average in one pass
IntSummaryStatistics stats = IntStream.of(4, 2, 5, 1).summaryStatistics();

System.out.println(stats.getSum());     // Output: 12
System.out.println(stats.getMax());     // Output: 5
System.out.println(stats.getAverage()); // Output: 3.0

Handwritten reductions are for the cases the library did not anticipate. When a shortcut exists, the shortcut wins on readability every time.

7. reduce Versus collect

This comparison comes up in almost every stream interview, and the answer is short once you see the difference.

7.1 Immutable Versus Mutable Reduction

reduce performs an immutable reduction. Each step creates a fresh result and throws the old one away. Adding two integers produces a third integer, and nobody edits anything.

collect performs a mutable reduction. It creates one container up front, then pours elements into it. Adding to a list changes that same list in place.

Watch what happens when you force reduce to build a list.

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

// Painful, and it copies the whole list on every element
List<Integer> evens = numbers.stream()
        .filter(n -> n % 2 == 0)
        .reduce(new ArrayList<Integer>(),
                (list, n) -> {
                    List<Integer> copy = new ArrayList<>(list);
                    copy.add(n);
                    return copy;
                },
                (a, b) -> {
                    List<Integer> copy = new ArrayList<>(a);
                    copy.addAll(b);
                    return copy;
                });

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

That code is correct, and it is awful. Every element triggers a full copy of the list built so far, so nine elements cost you nine allocations and a growing pile of garbage.

// One line, one container, no copying
List<Integer> evens = numbers.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toList());

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

Same answer, a fraction of the work, and it stays safe on a parallel stream because each thread fills its own container first.

7.2 Side by Side

Aspect reduce collect
Kind of reduction Immutable Mutable
Result each step A brand-new value The same container, updated
Best for Single values: sum, max, merged object Containers: List, Set, Map, String
Cost of building a list Copies on every element Appends in place
Parallel behaviour Merges partial values Merges partial containers
Typical call reduce(0, Integer::sum) collect(Collectors.toList())

7.3 A Rule of Thumb

Ask yourself one question: is the answer a single value or a container?

  • A number, a boolean, one merged object → reach for reduce
  • A List, Set, Map or joined String → reach for collect
  • Grouping or partitioning rows → always collect, never reduce

The Collectors class covers the container side in full, and grouping in Java 8 shows what downstream collectors can do.

8. reduce and Parallel Streams

A well-written reduce parallelises beautifully. A careless one produces answers that change from run to run. Here is what separates the two.

8.1 A Wrong Identity Breaks in Parallel

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

// 1 is NOT the identity for addition
int sequential = numbers.stream().reduce(1, Integer::sum);
System.out.println(sequential); // Output: 16  (15 + one stray 1)

int parallel = numbers.parallelStream().reduce(1, Integer::sum);
System.out.println(parallel);   // Output: 19 or 20, and it can vary

Sequentially the bad identity slips in once, so you are off by exactly one. That is wrong, but at least it is predictable.

In parallel, each chunk starts from the identity. Five chunks mean five stray ones. The fork-join framework picks the chunk count based on the data size and the machine, so the error itself moves around.

8.2 Subtraction Is Not Associative

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

int sequential = numbers.stream().reduce(0, (a, b) -> a - b);
System.out.println(sequential); // Output: -16

int parallel = numbers.parallelStream().reduce(0, (a, b) -> a - b);
System.out.println(parallel);   // Output: 6, and it depends on how the data splits

The sequential run computes ((((0-10)-3)-2)-1). A parallel run might compute (0-10)-3 and (0-2)-1 separately, then subtract one partial from the other. Different grouping, different answer.

Test your accumulator with pen and paper before you parallelise. Regroup the operands by hand and check the result stays the same.

8.3 Shared Mutable State Is Worse Still

The classic disaster looks harmless in a code review.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evens = new ArrayList<>();   // shared, mutable, unsynchronised

numbers.parallelStream()
       .filter(n -> n % 2 == 0)
       .forEach(n -> evens.add(n));         // several threads write at once

System.out.println(evens);
// Output: [6, 4, 2, 8]  most runs
// Output: [6, 2, 8]     occasionally, because ArrayList is not thread-safe

Nothing throws. Nothing logs a warning. One day an element simply goes missing, and you spend an afternoon staring at a stack trace that never appears.

Mutability on its own is fine. Shared mutability across threads is the problem. Let reduce or collect merge the partial results and the whole class of bug disappears. Our article on parallel streams in Java 8 goes deeper into when parallel execution actually pays off.

9. Common Mistakes and Pitfalls

Seven traps, each one seen in real code reviews.

9.1 Forgetting the Optional

The one-argument reduce returns an Optional, not a value. Printing it directly gives you Optional[15] in your log file, which looks odd to everyone reading it.

// Prints the wrapper, not the number
System.out.println(numbers.stream().reduce(Integer::sum)); // Output: Optional[15]

// Unwrap it properly
System.out.println(numbers.stream().reduce(Integer::sum).orElse(0)); // Output: 15

Avoid get() without a check. It throws NoSuchElementException on an empty stream, which is exactly the case the Optional existed to warn you about.

9.2 Picking the Wrong Identity

Zero for multiplication is the classic. Every product collapses to zero and the bug looks like bad data rather than bad code.

int wrong = numbers.stream().reduce(0, (a, b) -> a * b);
System.out.println(wrong); // Output: 0

int right = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(right); // Output: 120

9.3 Building Strings With reduce

We covered the cost in section 5.4. To repeat the short version: use Collectors.joining for text and keep reduce for values.

9.4 A Stateful Accumulator

int[] callCount = {0};

// The lambda touches something outside itself. Never do this.
int sum = numbers.parallelStream().reduce(0, (a, b) -> {
    callCount[0]++;
    return a + b;
});

Your accumulator should look at its two arguments and nothing else. External counters, caches and loggers all break under parallel execution.

9.5 Reaching for reduce When a Shortcut Exists

Java already named the common reductions. Use the name.

Handwritten reduce Better
reduce(0, (a, b) -> a + 1) count()
reduce(Integer::max) max(Comparator.naturalOrder())
reduce("", (a, b) -> a + b) collect(Collectors.joining())
mapToInt(x -> x).reduce(0, Integer::sum) mapToInt(x -> x).sum()

9.6 null Values in the Stream

A null element reaches your accumulator like any other value, and the first method call on it throws. Filter early.

List<String> words = Arrays.asList("Java", null, "reduce");

// Throws NullPointerException on the null element
// words.stream().reduce((a, b) -> a + b);

String safe = words.stream()
        .filter(Objects::nonNull)
        .reduce("", (a, b) -> a + b);

System.out.println(safe); // Output: Javareduce

9.7 Reusing a Stream

reduce is terminal, so the stream closes behind it. Calling a second operation on the same stream object throws IllegalStateException.

Stream<Integer> stream = numbers.stream();

int sum = stream.reduce(0, Integer::sum);
// int product = stream.reduce(1, (a, b) -> a * b);
// Throws: java.lang.IllegalStateException: stream has already been operated upon or closed

// Build a fresh stream instead
int product = numbers.stream().reduce(1, (a, b) -> a * b);

10. Practical Walkthrough: A Small Order Report

Let us pull the pieces together into one program you could paste into an IDE right now.

10.1 The Data

Four orders from an online store. Each one carries a customer, an item, a quantity and a unit price.

class Order {
    final String customer;
    final String item;
    final int quantity;
    final double unitPrice;

    Order(String customer, String item, int quantity, double unitPrice) {
        this.customer = customer;
        this.item = item;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    double total() {
        return quantity * unitPrice;
    }
}

10.2 The Reductions

Four questions, four reductions, each one using a different overload.

package com.javahandson.reduce;

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

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

        List<Order> orders = Arrays.asList(
                new Order("Amit",   "Keyboard", 2,  40.0),
                new Order("Sara",   "Monitor",  1, 180.0),
                new Order("Neha",   "Mouse",    3,  15.0),
                new Order("Vikram", "Laptop",   1, 900.0));

        // 1. Total revenue - identity overload, 0.0 is neutral for addition
        double revenue = orders.stream()
                .map(Order::total)
                .reduce(0.0, Double::sum);

        // 2. Biggest single order - Optional overload, no answer if empty
        Optional<Order> biggest = orders.stream()
                .reduce((a, b) -> a.total() >= b.total() ? a : b);

        // 3. Total units sold - three-argument overload, Order in, int out
        int units = orders.stream()
                .reduce(0,
                        (running, order) -> running + order.quantity,
                        Integer::sum);

        // 4. Item list - collect, because the answer is a container
        String items = orders.stream()
                .map(order -> order.item)
                .collect(Collectors.joining(", "));

        System.out.println("Revenue     : " + revenue);
        System.out.println("Units sold  : " + units);
        System.out.println("Items       : " + items);
        biggest.ifPresent(o ->
                System.out.println("Biggest     : " + o.customer + " (" + o.total() + ")"));
    }
}
Output:
Revenue     : 1205.0
Units sold  : 7
Items       : Keyboard, Monitor, Mouse, Laptop
Biggest     : Vikram (900.0)

10.3 Reading the Output

  • Revenue maps each order to a double first, then folds with 0.0 as the identity. Empty order book, zero revenue — a sensible answer.
  • Biggest keeps whole Order objects in play. With no orders there is no biggest, so Optional is honest about it.
  • Units needs the three-argument form. Elements are Order objects, the result is an int, and the combiner merges partial counts.
  • Items deliberately skips reduce. The answer is joined text, which is collect territory.

Change the list to orders.parallelStream() and every number stays the same. That is the payoff for associative accumulators and honest identities.

11. Interview Questions

Q: What is the reduce method in Java 8 streams?

A: It is a terminal operation that folds all the elements of a stream into a single result. You give it a rule for combining two values, and it applies that rule repeatedly until one value remains.

Q: What are the three overloads of reduce?

A: The first takes an identity and an accumulator and returns T. Version two takes only an accumulator and returns Optional<T>. The last one takes an identity, an accumulator and a combiner, and it can return a type U that differs from the element type.

Q: Why does reduce sometimes return an Optional?

A: Without an identity there is no answer for an empty stream. Java wraps the result in an Optional rather than returning null, so you have to decide what an empty stream should mean.

Q: What is an identity value in reduce?

A: It is the neutral starting value for your operation. Combining it with any element must give that element back unchanged. Zero works for addition, one works for multiplication, and an empty string works for concatenation.

Q: When does Java call the combiner function?

A: Only when there are partial results to merge, which in practice means parallel streams. A sequential stream produces one partial result, so Java skips the combiner entirely.

Q: What is the difference between reduce and collect?

A: reduce performs an immutable reduction and creates a new result at every step, which suits single values. collect performs a mutable reduction and fills one container in place, which suits lists, sets, maps and joined strings.

Q: Is reduce an intermediate or a terminal operation?

A: It is terminal. The pipeline executes as soon as you call reduce, and the stream closes afterwards. Any further operation on that same stream throws IllegalStateException.

Q: Why does my parallel reduce give a different answer every run?

A: Two causes explain almost every case. Either your identity is not truly neutral, so each chunk injects a stray value, or your accumulator is not associative and the answer depends on how the data splits.

Q: Can reduce change the type of the result?

A: Yes, but only through the three-argument overload. The accumulator folds an element of type T into a result of type U, and the combiner merges two U values.

Q: Is reduce a good way to join strings?

A: It works, but it allocates a new String at every step because Strings are immutable. Collectors.joining appends into a single buffer, so prefer that for anything larger than a handful of values.

Q: What must an accumulator satisfy for reduce to be correct?

A: It must be associative, so regrouping the operands never changes the answer. It must also be stateless, meaning it reads only its two arguments and never touches a variable outside the lambda.

12. Conclusion

Let us wrap up what we covered. The reduce method in Java 8 streams turns many values into one, and it does so with a starting value and a rule for combining two things.

Three overloads cover every case. The identity version always returns a value. Drop that identity and you get an Optional back, because an empty stream has no answer. The three-argument version lets the result type differ from the element type and supplies a combiner for parallel work.

Two properties keep a reduction honest: a truly neutral identity and an associative, stateless accumulator. Ignore either one and a parallel stream will happily give you a different answer on every run.

We also drew the line against collect. Single values belong to reduce, containers belong to collect, and forcing reduce to build a list copies the whole thing on every element.

Open your IDE and try the order report. Add an order, flip the stream to parallel, then deliberately break the identity and watch the total drift. Nothing teaches these rules faster than seeing them fail.

Further Reading

Leave a Comment