Back to Stream: map & Transform
Q11Question 11 of 34

Convert a List<Employee> to an IntStream of ages and compute sum, average, max, min

Key Concepts
  • `mapToInt(Employee::getAge)` uses a method reference as a `ToIntFunction` to produce an `IntStream` of ages.
  • `sum()` returns a primitive `int`; it is zero for an empty stream.
  • `average()` returns an `OptionalDouble`, and `max()`/`min()` return `OptionalInt`, so you must unwrap them safely.
  • Each of `sum`, `average`, `max`, `min` is a terminal operation, so a fresh stream is needed for each separate call.
  • `summaryStatistics()` returns all four results in a single pass, avoiding four separate traversals.
  • A method reference like `Employee::getAge` is clearer than a lambda `e -> e.getAge()` and expresses intent directly.

Definition

`employees.stream().mapToInt(Employee::getAge)` gives an `IntStream` of ages on which `sum()` returns an `int` and `average()`, `max()`, `min()` return optionals.

Converting to an IntStream of ages is a map-then-aggregate pattern.

  • mapToInt – takes a ToIntFunction and returns an IntStream; Employee::getAge is the function.
  • sum() – adds all ages and returns a primitive int.
  • average() – returns an OptionalDouble because an empty stream has no average.
  • max() and min() – return an OptionalInt, empty when the stream has no elements.

Because each aggregation is a terminal operation, you either recreate the stream per call or use summaryStatistics() once.

At National Bank of India, Pankaj asks Rajesh for four numbers about a team: the combined age, the average age, the oldest, and the youngest. Rajesh first copies just the ages onto a clean strip of paper (mapToInt(Employee::getAge)), ignoring names and salaries. From that strip he can add them, average them, and spot the highest and lowest at a glance. If the strip is blank because the team is empty, there is a total of zero but no meaningful average or extremes, which is why those three answers come wrapped as optionals.

Understanding the Concepts

How do you convert a List to an IntStream of ages?

Call stream() on the list and then mapToInt(Employee::getAge). The method reference is a ToIntFunction, so each employee is reduced to an int age and the pipeline becomes an IntStream.

JAVA
package com.bank.hr;

import java.util.List;
import java.util.stream.IntStream;

public class AgeExtractor {
    public static IntStream ageStream(List<Employee> employees) {
        return employees.stream().mapToInt(Employee::getAge);
    }
}

Why use a method reference instead of a lambda?

Employee::getAge and e -> e.getAge() compile to the same thing, but the method reference states the intent directly and has less noise. Both satisfy the ToIntFunction that mapToInt expects.

JAVA
// Method reference: concise, expresses "get the age"
employees.stream().mapToInt(Employee::getAge);

// Equivalent lambda: more characters, same behavior
employees.stream().mapToInt(e -> e.getAge());

How do you compute sum, average, max, and min?

Each is a terminal operation on IntStream. Remember that a stream can be consumed only once, so build a fresh stream for each call, or use summaryStatistics() to get everything at once.

JAVA
package com.bank.hr;

import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;

public class AgeAggregates {
    public static void main(String[] args) {
        List<Employee> team = List.of(
            new Employee(1L, "Rajesh", 35),
            new Employee(2L, "Priya", 29),
            new Employee(3L, "Pankaj", 42));

        int sum = team.stream().mapToInt(Employee::getAge).sum();
        OptionalDouble avg = team.stream().mapToInt(Employee::getAge).average();
        OptionalInt max = team.stream().mapToInt(Employee::getAge).max();
        OptionalInt min = team.stream().mapToInt(Employee::getAge).min();

        System.out.println("sum     = " + sum);
        System.out.println("average = " + avg.getAsDouble());
        System.out.println("max     = " + max.getAsInt());
        System.out.println("min     = " + min.getAsInt());
    }
}
TEXT
sum     = 106
average = 35.333333333333336
max     = 42
min     = 29

What do these methods return, and why the difference?

sum() always has an answer (zero when empty), so it returns a plain int. average(), max(), and min() have no answer for an empty stream, so they return optionals that force the caller to decide what to do when there is no data.

| Method | Return type | Empty stream result |
|--------|-------------|---------------------|
| sum() | int | 0 |
| average() | OptionalDouble | OptionalDouble.empty() |
| max() | OptionalInt | OptionalInt.empty() |
| min() | OptionalInt | OptionalInt.empty() |

JAVA
List<Employee> none = List.of();
System.out.println(none.stream().mapToInt(Employee::getAge).sum());          // 0
System.out.println(none.stream().mapToInt(Employee::getAge).max().isPresent()); // false

Why can you not reuse one IntStream for all four results?

A stream is consumed by its first terminal operation. Calling a second terminal operation on the same stream throws IllegalStateException.

JAVA
// WRONG: reusing a consumed stream
IntStream ages = team.stream().mapToInt(Employee::getAge);
int sum = ages.sum();          // consumes the stream
double avg = ages.average().getAsDouble(); // throws IllegalStateException
JAVA
// CORRECT option A: a fresh stream per aggregation
int sum = team.stream().mapToInt(Employee::getAge).sum();
double avg = team.stream().mapToInt(Employee::getAge).average().orElse(0);

How do you compute all four in a single pass?

summaryStatistics() returns an IntSummaryStatistics with count, sum, min, average, and max after one traversal, which is more efficient than four separate streams.

JAVA
package com.bank.hr;

import java.util.IntSummaryStatistics;
import java.util.List;

public class AgeSummary {
    public static void main(String[] args) {
        List<Employee> team = List.of(
            new Employee(1L, "Rajesh", 35),
            new Employee(2L, "Priya", 29),
            new Employee(3L, "Pankaj", 42));

        IntSummaryStatistics s = team.stream()
            .mapToInt(Employee::getAge)
            .summaryStatistics();
        System.out.printf("sum=%d avg=%.2f max=%d min=%d%n",
            s.getSum(), s.getAverage(), s.getMax(), s.getMin());
    }
}
TEXT
sum=106 avg=35.33 max=42 min=29

What are the common mistakes?

The first mistake is unwrapping an optional without handling the empty case.

JAVA
// WRONG: throws NoSuchElementException if the team is empty
int oldest = team.stream().mapToInt(Employee::getAge).max().getAsInt();
JAVA
// CORRECT: provide a default or branch on presence
int oldest = team.stream().mapToInt(Employee::getAge).max().orElse(0);

The second mistake is computing a sum or average with boxed Integer and reduce when IntStream already offers direct methods.

JAVA
// WRONG style: boxing path for a plain average
double avg = team.stream().map(Employee::getAge)
    .collect(java.util.stream.Collectors.averagingInt(a -> a));

// CORRECT: primitive stream, no boxing
double avg = team.stream().mapToInt(Employee::getAge).average().orElse(0);

What are the edge cases?

A single-element stream returns that element for both max and min and itself as the average. A stream with very large ages cannot realistically overflow for human ages, but summing arbitrary large ints should use mapToLong for safety.

JAVA
List<Employee> one = List.of(new Employee(9L, "Priya", 29));
System.out.println(one.stream().mapToInt(Employee::getAge).max().getAsInt()); // 29
System.out.println(one.stream().mapToInt(Employee::getAge).min().getAsInt()); // 29
System.out.println(one.stream().mapToInt(Employee::getAge).average().getAsDouble()); // 29.0

When should you use separate calls versus summaryStatistics?

Use separate calls when you need only one or two aggregates and clarity matters. Use summaryStatistics() when you need several at once or when the source is expensive to traverse.

JAVA
// Only the average is needed: a single direct call reads best
double avg = team.stream().mapToInt(Employee::getAge).average().orElse(0);

// Several needed and traversal is costly: one pass
IntSummaryStatistics all = team.stream().mapToInt(Employee::getAge).summaryStatistics();

What should a senior engineer know?

  • Terminal operation semantics. sum, average, max, min, and summaryStatistics all consume the stream; plan to rebuild or use one pass.
  • Primitive advantage. mapToInt avoids boxing, so the aggregation works directly on raw ints.
  • Rounding of average. average() returns a double; format it for display rather than storing it where exact values matter.
  • Parallelism. These reductions are associative and parallelize safely, and summaryStatistics merges partial statistics correctly across threads.

What are common interview follow-up questions?

  • "What does sum() return and what about average()?" Answer: sum() returns int; average() returns OptionalDouble.
  • "Why can you not call sum() and then average() on the same stream?" Answer: a stream is consumed by its first terminal operation and throws IllegalStateException on reuse.
  • "How do you get all four in one pass?" Answer: use summaryStatistics().
  • "Why a method reference over a lambda?" Answer: it is more concise and expresses intent while compiling to the same ToIntFunction.

Visual Overview

DIAGRAM
  List<Employee>
   [ (Rajesh,35), (Priya,29), (Pankaj,42) ]
        │
        │  stream().mapToInt(Employee::getAge)
        ▼
  IntStream  [ 35, 29, 42 ]
        │
        ├── sum()      -> int             106
        ├── average()  -> OptionalDouble   35.33
        ├── max()      -> OptionalInt      42
        └── min()      -> OptionalInt      29
DIAGRAM
  One stream, one terminal op
  IntStream ──sum()──▶ consumed
       └── average()  X  IllegalStateException (already used)

  Rebuild per call                 OR    one pass
  stream().mapToInt(..).sum()            IntSummaryStatistics s =
  stream().mapToInt(..).average()           stream().mapToInt(..).summaryStatistics();
  stream().mapToInt(..).max()            s.getSum()/getAverage()/getMax()/getMin()
  stream().mapToInt(..).min()

Key Takeaway

When Pankaj asks Rajesh for the combined, average, oldest, and youngest age of a team, Rajesh first copies just the ages onto a clean strip with mapToInt(Employee::getAge). That turns a List into an IntStream of raw ints, dropping the names and salaries that do not matter for this question. The method reference Employee::getAge reads as plain intent and is the same ToIntFunction a lambda would produce.

From that IntStream the four answers come from four terminal operations. sum() always has an answer and returns a plain int. average(), max(), and min() return optionals, because an empty team has a total of zero but no meaningful average or extremes. That is the API being honest instead of inventing a value, so you unwrap with orElse or check presence.

The catch is that a stream is consumed by its first terminal operation. You cannot call sum() and then average() on the same IntStream; the second call throws IllegalStateException. So either build a fresh stream for each aggregate, or ask for all of them at once with summaryStatistics(), which walks the data a single time and returns count, sum, min, average, and max together.

The primitive path matters too. Because mapToInt produces raw ints, the arithmetic runs without boxing each age into an Integer, which is both faster and lighter than the boxed Stream route.

In our banking work the habit is: map to the primitive you need, decide whether you want one aggregate or several, and when it is several, prefer the single-pass summaryStatistics.

Remember one sentence: mapToInt(Employee::getAge) gives an IntStream where sum() returns an int and average(), max(), min() return optionals, and each terminal call needs its own stream.

---

Share
Use ← → arrow keys to navigate