Definition
A primitive stream is a specialized stream of int, long, or double values.
- mapToInt – a method that maps each element to an
intand returns anIntStream. - IntStream – a stream of raw
intvalues with numeric operations thatStreamdoes not have. - sum() – returns a primitive
int(orlong/doublefor the other primitive streams). - average(), min(), max() – return an
OptionalDoubleorOptionalInt, because an empty stream has no meaningful result.
Using a primitive stream for numeric work avoids boxing each number into an Integer object.
At National Bank of India, Priya needs the total, average, youngest, and oldest age of a team. A Stream is like asking every clerk to write each age on its own paper envelope before you can add them, then opening each envelope again to read the number. An IntStream is like reading the ages straight off a single tally sheet: no envelopes to seal and unseal. The envelopes are Integer boxing, and skipping them is exactly what mapToInt buys you.
Understanding the Concepts
What is mapToInt and why use it?
mapToInt is an intermediate operation that takes a ToIntFunction and returns an IntStream. Every element is converted to a primitive int, so the resulting stream holds raw numbers rather than Integer objects.
package com.bank.hr;
import java.util.List;
import java.util.stream.IntStream;
public class AgeStats {
public static IntStream ages(List<Employee> employees) {
return employees.stream().mapToInt(Employee::getAge);
}
}
The benefit is that IntStream carries numeric terminal operations that a Stream or a Stream does not expose directly.
How do you compute total, average, min, and max?
IntStream gives you sum(), average(), min(), and max(). Note that sum() returns an int, while the other three return optionals because the stream might be empty.
package com.bank.hr;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
public class AgeReport {
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 total = team.stream().mapToInt(Employee::getAge).sum();
OptionalDouble average = team.stream().mapToInt(Employee::getAge).average();
OptionalInt youngest = team.stream().mapToInt(Employee::getAge).min();
OptionalInt oldest = team.stream().mapToInt(Employee::getAge).max();
System.out.println("Total = " + total);
System.out.println("Average = " + average.getAsDouble());
System.out.println("Min = " + youngest.getAsInt());
System.out.println("Max = " + oldest.getAsInt());
}
}
Total = 106
Average = 35.333333333333336
Min = 29
Max = 42
Why return OptionalDouble and OptionalInt instead of a plain number?
An empty stream has a well-defined sum of zero, but no defined average, minimum, or maximum. Returning an Optional forces the caller to handle the empty case instead of receiving a misleading default.
// Empty team: sum is 0, but average/min/max have no value
List<Employee> empty = List.of();
int total = empty.stream().mapToInt(Employee::getAge).sum(); // 0
OptionalDouble avg = empty.stream().mapToInt(Employee::getAge).average(); // OptionalDouble.empty
System.out.println(total); // 0
System.out.println(avg.isPresent()); // false
System.out.println(avg.orElse(0.0)); // 0.0, a value you chose
Why is IntStream preferred over Stream for numbers?
A Stream stores each number as a boxed Integer object on the heap. Every arithmetic step unboxes to int, computes, and reboxes. This costs extra memory and CPU. An IntStream keeps raw int values, so there is no boxing or unboxing in the pipeline.
| Aspect | Stream | IntStream |
|--------|-------------------|-------------|
| Element storage | boxed Integer objects | raw int values |
| Boxing per element | yes | no |
| Numeric ops | via reduce/mapToInt | built in: sum, average, min, max |
| Memory footprint | higher (object headers) | lower (primitives) |
| Typical use | general objects | numeric aggregation |
// Boxed: every age becomes an Integer, then reduce unboxes to add
int totalBoxed = team.stream()
.map(Employee::getAge) // Stream<Integer>
.reduce(0, Integer::sum);
// Primitive: no boxing, direct sum
int totalPrimitive = team.stream()
.mapToInt(Employee::getAge) // IntStream
.sum();
How does summaryStatistics() compute everything in one pass?
Calling sum(), average(), min(), and max() separately traverses the data four times. summaryStatistics() returns an IntSummaryStatistics holding count, sum, min, average, and max after a single pass.
package com.bank.hr;
import java.util.IntSummaryStatistics;
import java.util.List;
public class OnePassStats {
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 stats = team.stream()
.mapToInt(Employee::getAge)
.summaryStatistics();
System.out.println("Count = " + stats.getCount());
System.out.println("Sum = " + stats.getSum());
System.out.println("Min = " + stats.getMin());
System.out.println("Average = " + stats.getAverage());
System.out.println("Max = " + stats.getMax());
}
}
Count = 3
Sum = 106
Min = 29
Average = 35.333333333333336
Max = 42
How do you go back and forth between primitive and object streams?
Use mapToInt to drop into a primitive stream and boxed() to return to a Stream when you need object-based operations such as collect(toList()).
// Object -> primitive -> object
List<Integer> ages = team.stream()
.mapToInt(Employee::getAge) // IntStream
.boxed() // Stream<Integer>
.collect(java.util.stream.Collectors.toList());
What are the common mistakes?
The first mistake is calling getAsDouble or getAsInt on an optional without checking whether the stream was empty, which throws NoSuchElementException.
// WRONG: throws NoSuchElementException on an empty team
double avg = empty.stream().mapToInt(Employee::getAge).average().getAsDouble();
// CORRECT: supply a default for the empty case
double avg = empty.stream().mapToInt(Employee::getAge).average().orElse(0.0);
The second mistake is doing numeric work with boxed streams and reduce when IntStream already provides the operation.
// WRONG style: boxing then manual reduce for a simple sum
int total = team.stream().map(Employee::getAge).reduce(0, Integer::sum);
// CORRECT: primitive stream with sum()
int total = team.stream().mapToInt(Employee::getAge).sum();
What are the edge cases around empty streams and overflow?
An empty IntStream returns 0 from sum() and empty optionals from the others. Also, sum() on an IntStream returns an int and can overflow for very large totals; use mapToLong for sums that may exceed Integer.MAX_VALUE.
// Overflow risk: many large ints summed as int can wrap around
long safeTotal = transactions.stream()
.mapToLong(t -> t.getAmountInPaise()) // LongStream
.sum();
When should you use a primitive stream versus an object stream?
Use a primitive stream whenever the pipeline is fundamentally numeric: summing balances, averaging ages, finding the largest transaction. Use an object stream when you need to keep the full object, group, or collect into object-based collections.
// Numeric aggregation: primitive stream
double averageAge = team.stream().mapToInt(Employee::getAge).average().orElse(0);
// Keep objects: object stream
List<Employee> seniors = team.stream()
.filter(e -> e.getAge() >= 40)
.collect(java.util.stream.Collectors.toList());
What should a senior engineer know?
- Three primitive streams.
IntStream,LongStream, andDoubleStreamexist to avoid boxing for the three numeric primitives; there is noCharStreamorByteStream. - Money is special. For currency, ages are fine as
int, but money amounts belong inBigDecimal; primitive streams ofdoublerisk rounding errors, so sum money asBigDecimalwithreduce. - One pass with statistics. Prefer
summaryStatistics()over calling four terminal operations separately to avoid traversing the source multiple times. - Parallelism.
IntStream.sum()is associative and parallelizes cleanly;average()internally tracks count and sum so it also parallelizes correctly.
What are common interview follow-up questions?
- "What does
sum()return versusaverage()?" Answer:sum()returns a primitiveint;average()returns anOptionalDouble. - "Why an
Optionalfor average but not for sum?" Answer: an empty stream has sum zero but no defined average, min, or max. - "Why prefer
IntStreamoverStream?" Answer: it avoids boxing and unboxing and offers numeric operations directly. - "How do you compute all statistics in one pass?" Answer: use
summaryStatistics()returningIntSummaryStatistics. - "How do you avoid overflow when summing?" Answer: use
mapToLongso the sum is along.
Visual Overview
List<Employee>
│
│ stream()
▼
Stream<Employee>
│
│ mapToInt(Employee::getAge) <- drop boxing, go primitive
▼
IntStream [ 35, 29, 42 ] (raw ints, no Integer objects)
│
├── sum() -> int 106
├── average() -> OptionalDouble 35.33
├── min() -> OptionalInt 29
├── max() -> OptionalInt 42
└── summaryStatistics() -> IntSummaryStatistics (all in one pass)
Stream<Integer> (boxed path) IntStream (primitive path)
┌───────────────────────────┐ ┌───────────────────────────┐
│ [Integer] [Integer] [...] │ │ 35 29 42 │
│ unbox -> add -> rebox │ │ add directly, no boxing │
│ heap objects + GC │ │ raw ints on the stack │
└───────────────────────────┘ └───────────────────────────┘
slower, more memory faster, less memory
Key Takeaway
When Priya needs the total, average, youngest, and oldest age of a team, the clean path is employees.stream().mapToInt(Employee::getAge). That single step turns a stream of employees into an IntStream of raw ages. Instead of wrapping each age in an Integer envelope and reopening it to do math, the ages sit on one tally sheet ready to be added.
On that IntStream you get numeric operations for free. sum() gives the total as a plain int. average(), min(), and max() give results too, but wrapped in OptionalDouble or OptionalInt. The reason is honesty: an empty team has a sum of zero, but it has no average, no youngest, and no oldest, so the API makes you handle that case with orElse instead of handing you a misleading default.
The reason a primitive stream beats Stream for numbers is boxing. A boxed stream stores each number as a heap object and unboxes and reboxes around every operation, costing memory and CPU. A primitive stream skips all of that. When you need every statistic at once, summaryStatistics() computes count, sum, min, average, and max in a single pass.
Two cautions carry over to production. Guard the optionals so an empty stream does not throw NoSuchElementException, and remember that sum() on an IntStream can overflow, so switch to mapToLong for large totals. And while ages are fine as ints, money always belongs in BigDecimal.
Remember one sentence: use mapToInt to get a primitive IntStream, where sum() returns an int and average/min/max return optionals you must unwrap safely.
---