skip to content

Why do IntStream.min(), average(), etc. return OptionalInt/OptionalDouble instead of Optional<Integer> or a plain value?

level: seniorimportance: should knowfreq 38%

answer

  1. empty stream -> no min/max/average -> Optional
  2. primitive Optional avoids re-boxing the result
  3. OptionalInt/Long/Double, read via getAsInt()/orElse()
  4. average() -> OptionalDouble even on IntStream
  5. sum()/count() return plain values (identity 0), no Optional

basics

~20 s

A stream might be empty, so there may be no minimum or average to return. Optional represents 'maybe a value'. The primitive variants OptionalInt/OptionalDouble exist so the result stays unboxed — Optional<Integer> would force a wrapper object, defeating the point of using a primitive stream.

solid answer

~50 s

Operations like min()/max()/average() can be undefined: an empty stream has no minimum and no average, and returning a default like 0 would be a silent lie. So they return an Optional — a container that is either present or empty — forcing the caller to handle the no-element case. The reason it's OptionalInt/OptionalLong/OptionalDouble rather than Optional<Integer> is consistency with the primitive-stream goal of avoiding boxing: Optional<Integer> would wrap the result in an Integer, reintroducing the very allocation primitive streams exist to eliminate. The primitive optionals carry a raw value with no wrapper. You read them with getAsInt()/getAsDouble(), or safer orElse(default), orElseThrow(), ifPresent(...), isPresent(). Note average() returns OptionalDouble even on an IntStream, because a mean is fractional; sum() and count() do not return Optionals because an empty stream's sum is 0 and count is 0 — those are well-defined.

code

java · 16 lines
java
IntStream values = IntStream.of(7, 3, 9, 4);

OptionalInt min = values.min();     // present here
int m = min.orElse(Integer.MAX_VALUE); // safe extraction -> 3

// average() returns OptionalDouble even from an IntStream:
OptionalDouble avg = IntStream.of(7, 3, 9, 4).average();
double a = avg.orElseThrow();        // 5.75

// empty stream -> empty optional, not a fake value:
OptionalInt none = IntStream.empty().max();
System.out.println(none.isPresent()); // false
// none.getAsInt(); // would throw NoSuchElementException

// but sum() of empty is a well-defined 0 (no Optional):
int zero = IntStream.empty().sum();   // 0

go deeper

for a junior

Knows min()/average() return an Optional because the stream might be empty, and that you should use orElse to supply a default.

for a middle

Explains that OptionalInt/OptionalDouble avoid boxing the result and reads them with getAsInt()/orElse; knows average() yields OptionalDouble.

for a senior

Articulates the empty-stream identity rule (why sum/count return plain values but min/max/average return optionals) and the getAsX() vs orElseThrow safety trade-off.

for a principal

Frames it as API design: making absence type-visible without sacrificing the no-boxing invariant, and the deliberate minimalism of the primitive optionals (no map/filter).

## The problem these methods face: undefined results Some aggregate questions have **no answer** when there is no data: - What is the **minimum** of an empty set of numbers? Undefined. - The **maximum**? Undefined. - The **average**? Undefined (you'd divide by zero). A method must still return *something*. Two bad options are: (a) return a magic default like `0` or `-1` — a **silent lie** the caller may mistake for a real value; or (b) throw an exception for the empty case — heavy-handed for a routine situation. Java's chosen tool is **`Optional`**. ## What Optional is `Optional` is a small container that holds **either** one value (**present**) **or** nothing (**empty**). It makes 'there might be no result' part of the **type**, so the compiler nudges the caller to handle absence instead of getting a possibly-meaningless number. You never get a `null` and you can't accidentally read a value that isn't there without acknowledging it. ## Why a *primitive* Optional, not Optional<Integer> The generic `Optional<T>` holds a reference type `T`. For an `int` result that would be `Optional<Integer>` — and an `Integer` is a **boxed wrapper object** on the heap. But the whole reason you used an `IntStream` was to **avoid boxing**. Returning `Optional<Integer>` from `IntStream.min()` would re-box the result, undoing that effort and allocating an object for every aggregate. So the JDK ships **primitive-specialized optionals**: - `OptionalInt` (returned by `IntStream.min()`, `.max()`, `findFirst()`, etc.) - `OptionalLong` - `OptionalDouble` Each carries a **raw** `int`/`long`/`double` plus an 'is present' flag — **no wrapper object**. This keeps the no-boxing guarantee end-to-end. ## Which methods return what - `min()`, `max()` on `IntStream` → `OptionalInt` (could be empty). - `average()` → **`OptionalDouble`** *even on an `IntStream`*, because a mean is fractional, and empty input has no average. - `findFirst()`, `findAny()`, `reduce()` (the no-identity overload) → the matching primitive Optional, since there may be no element. - `sum()` → a plain `int`/`long`/`double`, **not** an Optional: the sum of an empty stream is well-defined as `0`. - `count()` → a plain `long`: empty count is `0`. So the rule is: a method returns an Optional exactly when the empty-stream answer would be **undefined**, and returns a plain value when there is a sensible identity (0 for sum/count). ## How you read a primitive Optional - `getAsInt()` / `getAsLong()` / `getAsDouble()` — extract the raw value, but **throws `NoSuchElementException` if empty**. (Note the name differs from `Optional.get()`.) - `orElse(default)` — value if present, else your fallback. Safe. - `orElseThrow()` / `orElseThrow(supplier)` — value or a chosen exception. - `isPresent()` / `isEmpty()` — test before reading. - `ifPresent(consumer)` / `ifPresentOrElse(consumer, runnable)` — act only when present. Idiomatic, safe usage: ```java int min = numbers.stream().mapToInt(Integer::intValue) .min() .orElse(0); // explicit fallback for empty double avg = numbers.stream().mapToInt(Integer::intValue) .average() .orElseThrow(); // 'must not be empty' contract ``` ## The gotcha Calling `getAsInt()` (or `getAsDouble()`) on a possibly-empty result without checking is the primitive-stream version of an NPE: it throws `NoSuchElementException`. Prefer `orElse`/`orElseThrow`/`ifPresent`. And remember the method names: it's `getAsInt()`, not `get()`, and these optionals do **not** have `map`/`filter` like the full `Optional<T>` (the primitive optionals are deliberately minimal).

  • Why does sum() return a plain int but min() returns OptionalInt?
    Because the sum of an empty stream is well-defined as the identity 0, so a plain value is honest. The minimum of an empty stream is genuinely undefined — there's no element — so it must signal absence via OptionalInt rather than invent a value.
  • What's the safest way to extract the value from an OptionalDouble returned by average()?
    Avoid getAsDouble() unless you've checked presence. Prefer orElse(fallback) for a default, orElseThrow() when emptiness is a contract violation, or ifPresent(...) to act only when a value exists. getAsDouble() on an empty OptionalDouble throws NoSuchElementException.

saying these in an interview costs you the question

  • Expecting min()/average() to return Optional<Integer> — they return primitive optionals.
  • Calling getAsInt() on a possibly-empty result without a guard — throws NoSuchElementException.
  • Thinking average() returns OptionalInt — it returns OptionalDouble (means are fractional).
  • Expecting sum() to return an Optional — it returns a plain value (0 for empty).

context