Back to Optional
Q9Question 9 of 9

Compute average salary using DoubleStream

Key Concepts
  • mapToDouble() converts an object stream to a primitive DoubleStream, avoiding autoboxing overhead
  • DoubleStream.average() returns OptionalDouble because the average of zero elements is undefined
  • orElse(0.0) safely unwraps the OptionalDouble with a fallback for the empty case
  • getAsDouble() on an empty OptionalDouble throws NoSuchElementException, so prefer orElse or orElseGet
  • DoubleStream also offers sum(), min(), max(), and summaryStatistics() for other aggregations
  • For exact money calculations use BigDecimal; DoubleStream averages are suitable for dashboards and summaries

Definition

mapToDouble(...).average() returns an OptionalDouble, and orElse(0.0) provides a safe default when the stream is empty.
  • mapToDouble(ToDoubleFunction) converts an object stream into a primitive DoubleStream, avoiding autoboxing.
  • average() is a terminal operation that returns OptionalDouble because the average of zero elements is mathematically undefined.
  • orElse(0.0) unwraps the OptionalDouble, returning the computed average when present and the supplied default when the stream is empty.
  • getAsDouble() throws NoSuchElementException on an empty OptionalDouble, so orElse or orElseGet is the safe choice.

Priya at the National Bank of India adds up everyone's salary and divides by the head count to compute an average. But if the team is temporarily empty, there is no one to average, so she agrees in advance that an empty team means zero. The orElse call is that pre-agreed fallback.

Understanding the Concepts

What is DoubleStream and why use it for salary averages?

DoubleStream is a primitive specialisation of Stream that works with unboxed double values. When you call mapToDouble(Employee::getSalaryAsDouble) on a stream of employees, each employee's salary is projected into a primitive double without creating Double wrapper objects. This avoids the overhead of autoboxing and unboxing, which matters when processing thousands of payroll records.

The average() terminal operation on a DoubleStream returns an OptionalDouble rather than a plain double. The reason is simple: the average of zero elements is mathematically undefined (division by zero), so the API forces you to decide what an empty average means instead of silently returning NaN or throwing.

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.DoubleStream;

public class AverageSalary {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Rajesh", "Loans", 75000.00),
            new Employee("Priya",  "Loans", 82000.00),
            new Employee("Amit",   "Loans", 68000.00)
        );

        double avgSalary = employees.stream()
            .mapToDouble(Employee::getSalaryAsDouble)
            .average()
            .orElse(0.0);

        System.out.println("Average salary: " + avgSalary);
    }
}
TEXT
Average salary: 75000.0

Why does average() return OptionalDouble instead of double?

The average of an empty collection is undefined. If average() returned a plain double, it would have to pick an arbitrary value like 0.0 or NaN, which could silently corrupt a report. By returning OptionalDouble, the API makes you handle the empty case explicitly.

JAVA
// Empty stream: no employees
OptionalDouble result = DoubleStream.empty().average();
System.out.println(result.isPresent()); // false
System.out.println(result.orElse(0.0)); // 0.0

What is the difference between orElse and getAsDouble?

| Method | Empty behaviour | Use when |
|---|---|---|
| orElse(0.0) | Returns the fallback value | You have a safe default |
| orElseGet(supplier) | Calls the supplier lazily | Default is expensive to compute |
| getAsDouble() | Throws NoSuchElementException | You are certain the stream is non-empty |
| ifPresent(consumer) | Does nothing if empty | You only act when a value exists |

JAVA
// WRONG: getAsDouble on a potentially empty stream
double wrong = employees.stream()
    .mapToDouble(Employee::getSalaryAsDouble)
    .average()
    .getAsDouble();  // throws if employees is empty!

// CORRECT: orElse provides a safe default
double correct = employees.stream()
    .mapToDouble(Employee::getSalaryAsDouble)
    .average()
    .orElse(0.0);

How is mapToDouble different from map?

map produces a Stream (boxed objects), while mapToDouble produces a primitive DoubleStream. The primitive version avoids creating wrapper objects and gives access to numeric terminal operations like sum(), average(), min(), max(), and summaryStatistics().

JAVA
// Boxed: Stream<Double> - no average() method
Stream<Double> boxed = employees.stream()
    .map(Employee::getSalaryAsDouble);

// Primitive: DoubleStream - has average(), sum(), etc.
DoubleStream primitive = employees.stream()
    .mapToDouble(Employee::getSalaryAsDouble);

double avg = primitive.average().orElse(0.0);

What other aggregations does DoubleStream offer?

Beyond average(), DoubleStream provides several numeric terminal operations that all handle the empty case cleanly.

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.DoubleStream;

public class SalaryAggregations {
    public static void main(String[] args) {
        double[] salaries = {75000.0, 82000.0, 68000.0, 91000.0};

        System.out.println("Sum:     " + DoubleStream.of(salaries).sum());
        System.out.println("Average: " + DoubleStream.of(salaries).average().orElse(0.0));
        System.out.println("Min:     " + DoubleStream.of(salaries).min().orElse(0.0));
        System.out.println("Max:     " + DoubleStream.of(salaries).max().orElse(0.0));
        System.out.println("Count:   " + DoubleStream.of(salaries).count());
    }
}
TEXT
Sum:     316000.0
Average: 79000.0
Min:     68000.0
Max:     91000.0
Count:   4

What are common mistakes?

Mistake 1: calling getAsDouble without checking for empty.

JAVA
// WRONG: crashes on empty list
List<Employee> empty = List.of();
double avg = empty.stream()
    .mapToDouble(Employee::getSalaryAsDouble)
    .average()
    .getAsDouble();  // NoSuchElementException!

// CORRECT: use orElse
double safe = empty.stream()
    .mapToDouble(Employee::getSalaryAsDouble)
    .average()
    .orElse(0.0);

Mistake 2: reusing a DoubleStream after a terminal operation.

JAVA
// WRONG: stream already consumed
DoubleStream ds = employees.stream()
    .mapToDouble(Employee::getSalaryAsDouble);
double sum = ds.sum();       // OK
double avg = ds.average().orElse(0.0);  // IllegalStateException!

// CORRECT: use summaryStatistics for multiple aggregations
var stats = employees.stream()
    .mapToDouble(Employee::getSalaryAsDouble)
    .summaryStatistics();
System.out.println("Sum: " + stats.getSum());
System.out.println("Avg: " + stats.getAverage());

When should you use BigDecimal instead of DoubleStream?

Double arithmetic can introduce tiny rounding errors because it uses IEEE 754 floating-point representation. For display averages on dashboards, DoubleStream is perfectly fine. For authoritative accounting where every paisa matters, use BigDecimal.

JAVA
import java.math.BigDecimal;
import java.math.RoundingMode;

// Exact average using BigDecimal
BigDecimal total = employees.stream()
    .map(Employee::getSalary)  // returns BigDecimal
    .reduce(BigDecimal.ZERO, BigDecimal::add);

BigDecimal exactAvg = employees.isEmpty()
    ? BigDecimal.ZERO
    : total.divide(BigDecimal.valueOf(employees.size()),
                   2, RoundingMode.HALF_UP);

System.out.println("Exact average: " + exactAvg);

What should a senior engineer know?

  • OptionalDouble vs Optional: OptionalDouble is a primitive specialisation with no boxing. It has orElse(double) and getAsDouble(), not get() and orElse(T).
  • Parallel safety: average() on a parallel DoubleStream is safe because it uses a combine-then-divide strategy internally.
  • summaryStatistics(): when you need count, sum, min, max, and average together, use summaryStatistics() to compute all five in a single pass instead of creating five separate streams.
  • Empty-stream behaviour: sum() returns 0.0, count() returns 0, but min() and max() return empty OptionalDouble with sentinel values. Always check count before trusting min or max.

What are common interview follow-up questions?

  • "What is the difference between orElse and orElseGet?" orElse always evaluates its argument; orElseGet calls the supplier only when empty.
  • "How do you compute average salary per department?" Use groupingBy(dept, averagingDouble(salary)).
  • "What if some employees have null salaries?" Filter nulls first or map to 0.0 as a policy decision.

Visual Overview

DIAGRAM
  List<Employee>
  [Rajesh:75000, Priya:82000, Amit:68000]
       │
       │  .mapToDouble(Employee::getSalaryAsDouble)
       ▼
  DoubleStream [75000.0, 82000.0, 68000.0]
       │
       │  .average()
       ▼
  OptionalDouble[75000.0]   (sum=225000 / count=3)
       │
       │  .orElse(0.0)
       ▼
  double: 75000.0

  Empty case:
  DoubleStream []  →  average()  →  OptionalDouble.empty
                                          │
                                          │  .orElse(0.0)
                                          ▼
                                     double: 0.0

Key Takeaway

To compute an average salary, project employees into a primitive DoubleStream with mapToDouble, which avoids autoboxing, and call average(). The key design point is that average() returns an OptionalDouble rather than a plain double, because the average of zero elements is mathematically undefined, so the API forces you to decide what an empty collection means.

Unwrap the OptionalDouble with orElse(0.0), so a populated team returns its real average and an empty team returns zero instead of throwing. Avoid getAsDouble() on potentially empty streams because it throws NoSuchElementException.

For dashboard averages at the National Bank of India, DoubleStream is the right tool. For authoritative accounting where every paisa matters, switch to BigDecimal with explicit rounding. The choice between them is a precision decision driven by whether the number is displayed or moves real money.

Remember one sentence: mapToDouble().average() returns OptionalDouble because the average of zero elements is undefined, and orElse(0.0) provides the safe default.

---

Share
Use ← → arrow keys to navigate