Back to Stream: Sorting & Comparator
Q11Question 11 of 23

Find the highest-paid employee using reduce with BinaryOperator.maxBy and a Comparator

Key Concepts
  • `Stream.reduce(BinaryOperator)` with no identity folds the stream pairwise and returns an `Optional`, empty if the stream is empty.
  • `BinaryOperator.maxBy(comparator)` returns the greater of two elements, so reduce keeps the maximum across the whole stream.
  • `Stream.max(comparator)` is the direct shorthand and the clearest choice for a plain maximum.
  • `Collectors.maxBy(comparator)` is a collector form, useful as a downstream inside `groupingBy` for the maximum per group.
  • All three return an `Optional` because an empty stream has no maximum, so the caller must handle emptiness.
  • The comparator must be consistent, and because the operations are associative they parallelize correctly.
  • `BinaryOperator.minBy` mirrors `maxBy` to select the minimum element.

Definition

`reduce(maxBy(comparator))` keeps the larger of each pair to find the maximum; `Stream.max(comparator)` is the direct shorthand.

Several tools find an extreme element, built on the same idea.

  • Stream.reduce(BinaryOperator) – folds the stream by combining elements pairwise and returns an Optional, empty when the stream is empty.
  • BinaryOperator.maxBy(comparator) – an operator that returns the greater of its two arguments by the comparator.
  • Stream.max(comparator) – a dedicated terminal returning Optional of the maximum, the clearest shorthand.
  • Collectors.maxBy(comparator) – a collector form, useful as a downstream inside groupingBy.

To find the top earner, Priya compares two employees at a time and keeps whoever earns more, then compares that winner against the next person, and so on down the roster. The last one standing is the highest-paid. maxBy is the keep-the-bigger-of-these-two rule she applies at each step.

Understanding the Concepts

What is Stream.reduce with a single binary operator?

Stream.reduce(BinaryOperator op) with no identity folds the stream: it applies the operator to the running result and each next element, and returns an Optional. The result is empty when the stream has no elements, because there is nothing to fold.

JAVA
package com.bank.analytics;

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

public class ReduceBasics {
    public static void main(String[] args) {
        Optional<Integer> sum = List.of(10, 20, 30).stream()
                .reduce((a, b) -> a + b);
        System.out.println(sum); // Optional[60]

        Optional<Integer> none = List.<Integer>of().stream()
                .reduce((a, b) -> a + b);
        System.out.println(none); // Optional.empty
    }
}
TEXT
Optional[60]
Optional.empty

What is BinaryOperator.maxBy?

BinaryOperator.maxBy(comparator) returns an operator that, given two elements, returns the greater according to the comparator. Using it as the reduce operator makes each fold step keep the larger element, so the final result is the maximum of the whole stream.

JAVA
package com.bank.analytics;

import com.bank.model.Employee;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;

public class HighestPaidReduce {
    public Optional<Employee> highestPaid(List<Employee> employees) {
        Comparator<Employee> bySalary = Comparator.comparing(Employee::salary);
        return employees.stream().reduce(BinaryOperator.maxBy(bySalary));
    }

    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee(1, "Rajesh", "Trading", new BigDecimal("1200000")),
                new Employee(2, "Priya", "Risk", new BigDecimal("850000")),
                new Employee(3, "Pankaj", "Management", new BigDecimal("2000000"))
        );
        new HighestPaidReduce().highestPaid(employees)
                .ifPresent(e -> System.out.println(e.name() + " " + e.salary()));
    }
}
TEXT
Pankaj 2000000

The Employee record:

JAVA
package com.bank.model;

import java.math.BigDecimal;

public record Employee(long id, String name, String department, BigDecimal salary) { }

How does reduce with maxBy select an extreme step by step?

Reduce starts with the first element as the running result, then at each step hands the running result and the next element to maxBy, which returns the greater. The running result is therefore always the maximum seen so far, and after the last element it is the overall maximum.

JAVA
// Conceptually, for salaries [1200000, 850000, 2000000]:
// step 1: max(1200000, 850000)  = 1200000
// step 2: max(1200000, 2000000) = 2000000  -> final

What is the Stream.max shorthand?

Stream.max(comparator) is a dedicated terminal that returns the maximum element wrapped in an Optional. It expresses the intent directly and is the clearest choice in most code; under the hood it does the same pairwise selection as reduce with maxBy.

JAVA
package com.bank.analytics;

import com.bank.model.Employee;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class HighestPaidMax {
    public Optional<Employee> highestPaid(List<Employee> employees) {
        return employees.stream().max(Comparator.comparing(Employee::salary));
    }
}
TEXT
Optional[Employee[id=3, name=Pankaj, ...]]

What is the Collectors.maxBy collector form?

Collectors.maxBy(comparator) is a collector that produces the maximum element as an Optional. On its own it is just another way to write Stream.max, but its real value is as a downstream collector inside groupingBy, where it yields the maximum per group.

JAVA
package com.bank.analytics;

import com.bank.model.Employee;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.maxBy;

public class TopEarnerPerDepartment {
    public Map<String, Optional<Employee>> topPerDept(List<Employee> employees) {
        return employees.stream()
                .collect(groupingBy(Employee::department,
                        maxBy(Comparator.comparing(Employee::salary))));
    }
}
TEXT
{Management=Optional[Pankaj], Risk=Optional[Priya], Trading=Optional[Rajesh]}

How do the three approaches compare?

| Approach | Form | Returns | Best use |
| --- | --- | --- | --- |
| reduce(BinaryOperator.maxBy(cmp)) | terminal | Optional | shows the mechanism; composes with other operators |
| Stream.max(cmp) | terminal | Optional | the clearest choice for a plain maximum |
| Collectors.maxBy(cmp) | collector | Optional | downstream inside groupingBy for per-group max |

JAVA
// All three produce the same highest-paid employee
Comparator<Employee> cmp = Comparator.comparing(Employee::salary);
Optional<Employee> a = employees.stream().reduce(BinaryOperator.maxBy(cmp));
Optional<Employee> b = employees.stream().max(cmp);
Optional<Employee> c = employees.stream().collect(Collectors.maxBy(cmp));

What are common mistakes?

The first mistake is calling .get() on the result without handling an empty stream, which throws NoSuchElementException.

JAVA
// WRONG: get on a possibly-empty result
Employee top = employees.stream().max(Comparator.comparing(Employee::salary)).get();
// NoSuchElementException when employees is empty
JAVA
// CORRECT: handle the empty case
Employee top = employees.stream()
        .max(Comparator.comparing(Employee::salary))
        .orElseThrow(() -> new IllegalStateException("no employees to rank"));

The second mistake is using reduce with an identity that is not a real neutral element, which corrupts the result. For finding a maximum, prefer the no-identity reduce that returns an Optional, or Stream.max.

JAVA
// WRONG: a fake identity can win incorrectly or skew the fold
Employee sentinel = new Employee(0, "", "", new BigDecimal("-1"));
Employee top = employees.stream()
        .reduce(sentinel, BinaryOperator.maxBy(Comparator.comparing(Employee::salary)));
// returns the sentinel for an empty stream, which the caller may not expect
JAVA
// CORRECT: no identity, get an Optional that is empty when there is nothing
Optional<Employee> top = employees.stream()
        .reduce(BinaryOperator.maxBy(Comparator.comparing(Employee::salary)));

What are edge cases, parallelism, and the minBy mirror?

All three return an Optional because an empty stream has no maximum, so the caller must handle emptiness. For ties, these operations keep one element per their defined behavior, so the comparator should be consistent to make the choice predictable. Because maxBy is associative, reduce and max parallelize correctly, giving the same result sequentially or in parallel.

JAVA
// Parallel gives the same maximum because maxBy is associative
Optional<Employee> top = employees.parallelStream()
        .reduce(BinaryOperator.maxBy(Comparator.comparing(Employee::salary)));

BinaryOperator.minBy mirrors maxBy to select the minimum, and Stream.min is its shorthand.

JAVA
// Lowest-paid employee
Optional<Employee> lowest = employees.stream()
        .reduce(BinaryOperator.minBy(Comparator.comparing(Employee::salary)));
Optional<Employee> lowestShort = employees.stream()
        .min(Comparator.comparing(Employee::salary));

When should you use each, and what are the follow-ups?

Prefer Stream.max for a plain maximum because it reads clearly. Use Collectors.maxBy when you need the maximum as a downstream inside groupingBy, such as the top earner per department. Reach for reduce with maxBy when you want to show or reason about the underlying mechanism, or when you compose the operator with other reductions. Interviewers often ask why all three return an Optional (an empty stream has no maximum), how to get the per-group maximum (groupingBy with maxBy downstream), and whether these parallelize safely (yes, because the operator is associative).

Visual Overview

DIAGRAM
  reduce(BinaryOperator.maxBy(bySalary))

  salaries: [1.2M] [0.85M] [2.0M]

     running = 1.2M
        │ maxBy(1.2M, 0.85M) = 1.2M
        ▼
     running = 1.2M
        │ maxBy(1.2M, 2.0M) = 2.0M
        ▼
     running = 2.0M  -> Optional[2.0M employee]
DIAGRAM
  three ways to the same maximum

  ┌────────────────────────────┬──────────────┬──────────────────────┐
  │ reduce(maxBy(cmp))         │ Stream.max   │ Collectors.maxBy     │
  │ terminal, Optional<T>      │ terminal     │ collector            │
  │ shows the mechanism        │ clearest     │ downstream in        │
  │                            │              │ groupingBy -> per    │
  │                            │              │ group maximum        │
  └────────────────────────────┴──────────────┴──────────────────────┘
DIAGRAM
  per-group maximum with groupingBy

  employees
     │ groupingBy(dept, maxBy(bySalary))
     ▼
  ┌───────────────┬──────────────────────┐
  │ Management     │ Optional[Pankaj]     │
  │ Risk           │ Optional[Priya]      │
  │ Trading        │ Optional[Rajesh]     │
  └───────────────┴──────────────────────┘

Key Takeaway

Stream.reduce with a single binary operator folds the stream by combining elements pairwise and returns an Optional, which is empty when the stream is empty. If I use BinaryOperator.maxBy of a salary comparator as that operator, each step keeps the greater of two employees, so the final result is the highest-paid employee wrapped in an Optional. This form is valuable because it shows the underlying mechanism of how an extreme is selected.

In my banking work at the National Bank of India I identified the single highest-paid employee for an executive compensation review. Because the roster could in principle be empty, I wanted an Optional rather than a value that might not exist, and reduce with maxBy gave me exactly that, returning empty for an empty list and the top earner otherwise.

The senior point I always make is that there are clearer shorthands for the same result and I pick the right one for the context. Stream.max of a comparator is a dedicated terminal and is the clearest choice for a plain maximum, while Collectors.maxBy is a collector I reach for when I need the maximum as a downstream inside groupingBy, such as the top earner per department. All three return an Optional because an empty stream has no maximum, so I always handle the empty case, and because the operator is associative they parallelize correctly. BinaryOperator.minBy and Stream.min mirror all of this for the minimum.

Think of Priya walking the roster: she holds the current top earner, compares the next person, keeps whoever earns more, and the last one left standing is the highest-paid.

Remember one sentence: reduce(BinaryOperator.maxBy(comparator)) keeps the larger of each pair to find the maximum, and Stream.max is the clear shorthand while Collectors.maxBy is the per-group form.

---

Share
Use ← → arrow keys to navigate