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.