The Streams API

Numeric Streams

15 min Lesson 8 of 13

Numeric Streams

The general-purpose Stream<T> is great for objects, but it carries a hidden cost when working with numbers: every primitive int or long must be boxed into an Integer or Long object. Boxing allocates heap objects and puts pressure on the garbage collector. For numeric pipelines that process thousands or millions of values, the overhead is real.

Java solves this with primitive-specialised stream types: IntStream, LongStream, and DoubleStream. They are exactly like Stream<T> but work directly on raw primitives — no boxing, no wrapper objects.

Creating an IntStream

The most common ways to get an IntStream:

import java.util.stream.IntStream; // 1. Closed range [1, 5] — includes both endpoints IntStream closed = IntStream.rangeClosed(1, 5); // 1 2 3 4 5 // 2. Half-open range [0, 5) — excludes the upper bound (like a for-loop index) IntStream open = IntStream.range(0, 5); // 0 1 2 3 4 // 3. Explicit values IntStream explicit = IntStream.of(10, 20, 30); // 4. From an int array int[] arr = {3, 1, 4, 1, 5}; IntStream fromArray = Arrays.stream(arr); // 5. From an object stream via mapToInt List<String> words = List.of("apple", "banana", "kiwi"); IntStream lengths = words.stream().mapToInt(String::length); // 5 6 4
range vs rangeClosed: IntStream.range(0, n) mirrors a standard for (int i = 0; i < n; i++) loop — the upper bound is excluded. rangeClosed(1, n) mirrors a for (int i = 1; i <= n; i++) loop. Pick the one that matches your mental model for the problem.

LongStream — when int is not enough

LongStream has the same API as IntStream but works with long primitives. Use it whenever your values can exceed roughly 2.1 billion — for example, file sizes in bytes, Unix epoch timestamps, or row IDs from a large database.

import java.util.stream.LongStream; // Sum numbers 1 to 1,000,000 — result would overflow an int long sum = LongStream.rangeClosed(1, 1_000_000).sum(); System.out.println(sum); // 500000500000 // Current time in ms, then add offsets long now = System.currentTimeMillis(); LongStream.of(now, now + 3600_000L, now + 7200_000L) .forEach(System.out::println);

Built-in terminal operations: sum, average, min, max

Because the stream type already knows it holds numbers, it can offer terminal operations that make no sense on a generic Stream<T>:

IntStream scores = IntStream.of(88, 92, 75, 95, 60); int total = IntStream.of(88, 92, 75, 95, 60).sum(); // 410 int highest = IntStream.of(88, 92, 75, 95, 60).max().getAsInt(); // 95 int lowest = IntStream.of(88, 92, 75, 95, 60).min().getAsInt(); // 60 // average returns OptionalDouble because an empty stream has no average double avg = IntStream.of(88, 92, 75, 95, 60).average().getAsDouble(); // 82.0 System.out.println("Total: " + total); System.out.println("Highest: " + highest); System.out.println("Average: " + avg);
Streams are consumed after one terminal operation. In the examples above each pipeline is written from scratch precisely because you cannot reuse a stream. If you need multiple statistics, call summaryStatistics() instead (see below) — it makes a single pass.

summaryStatistics — one pass, everything at once

Calling sum(), then average(), then max() on the same data would require three separate streams and three passes. summaryStatistics() computes all five statistics in a single pass:

import java.util.IntSummaryStatistics; IntSummaryStatistics stats = IntStream.of(88, 92, 75, 95, 60) .summaryStatistics(); System.out.println("Count: " + stats.getCount()); // 5 System.out.println("Sum: " + stats.getSum()); // 410 System.out.println("Min: " + stats.getMin()); // 60 System.out.println("Max: " + stats.getMax()); // 95 System.out.println("Avg: " + stats.getAverage()); // 82.0

LongStream has the equivalent LongSummaryStatistics and DoubleStream has DoubleSummaryStatistics. Each exposes the same five accessors.

When to use summaryStatistics: whenever your code needs more than one numeric aggregate from the same dataset. It is always more efficient than multiple terminal operations on reconstructed streams, and it keeps the code concise.

Converting between primitive and object streams

Sometimes you start with an object stream and need a numeric one, or vice versa:

import java.util.List; import java.util.stream.Collectors; List<String> names = List.of("Alice", "Bob", "Charlie", "Dan"); // Object stream --> IntStream IntStream nameLengths = names.stream().mapToInt(String::length); // IntStream --> Stream<Integer> (boxes each int) Stream<Integer> boxed = IntStream.range(1, 5).boxed(); // IntStream --> List<Integer> List<Integer> list = IntStream.rangeClosed(1, 5) .boxed() .collect(Collectors.toList()); System.out.println(list); // [1, 2, 3, 4, 5]

Practical example: grade analyser

Putting it all together — a realistic snippet that analyses a list of exam scores:

import java.util.IntSummaryStatistics; import java.util.List; public class GradeAnalyser { public static void main(String[] args) { List<Integer> grades = List.of(73, 88, 55, 91, 67, 84, 49, 95, 78, 62); IntSummaryStatistics stats = grades.stream() .mapToInt(Integer::intValue) .summaryStatistics(); long passing = grades.stream() .mapToInt(Integer::intValue) .filter(g -> g >= 60) .count(); System.out.printf("Students : %d%n", stats.getCount()); System.out.printf("Highest : %d%n", stats.getMax()); System.out.printf("Lowest : %d%n", stats.getMin()); System.out.printf("Average : %.1f%n", stats.getAverage()); System.out.printf("Passing : %d / %d%n", passing, stats.getCount()); } }

Summary

Use IntStream and LongStream whenever your pipeline works with primitive integers — they avoid boxing overhead, expose convenient terminal operations (sum(), average(), min(), max()), and offer summaryStatistics() to collect all five aggregates in one efficient pass. Bridge back to an object stream with .boxed() whenever you need a List or further object-stream operations.