Java DoubleStream: Primitive Double Streams Explained
java doublestream: Learn how to use Java DoubleStream for efficient primitive double processing, including creation, operations, performance tradeoffs, and edge cases.
The term java doublestream refers to DoubleStream, a specialized stream type in the java.util.stream package that operates on primitive double values. Unlike Stream<Double>, which boxes each value into a Double object, DoubleStream keeps values as raw primitives throughout the pipeline. This distinction matters for both memory footprint and iteration overhead, especially when a pipeline processes large collections of floating-point data.
DoubleStream follows the same general shape as the other primitive stream types (IntStream and LongStream): you create a stream, apply zero or more intermediate operations, and finish with a terminal operation that produces a result or side effect.
Creating a DoubleStream
The DoubleStream interface provides several factory methods for constructing streams from different sources.
DoubleStream.of(1.5, 2.5, 3.5); DoubleStream.iterate(0.0, d -> d + 0.5).limit(10); DoubleStream.generate(Math::random).limit(5); DoubleStream.builder() .add(1.0) .add(2.0) .add(3.0) .build();
of is the simplest option when you have a fixed set of values. iterate produces an infinite stream from a seed and an increment function, so it must be paired with limit to avoid an unbounded pipeline. generate works similarly for values produced by a supplier. The builder is useful when the number of elements is not known until runtime.
You can also convert an array or a collection into a DoubleStream:
double[] values = {1.0, 2.0, 3.0}; DoubleStream stream = Arrays.stream(values); List<Double> list = List.of(1.0, 2.0, 3.0); DoubleStream fromList = list.stream().mapToDouble(Double::doubleValue);
Arrays.stream accepts a double[] directly. For a List<Double>, you map each boxed value back to a primitive with mapToDouble.
Intermediate Operations on DoubleStream
Intermediate operations transform the stream and return a new DoubleStream. They are lazy; nothing executes until a terminal operation is invoked.
DoubleStream.of(1.0, 2.0, 3.0, 4.0) .filter(d -> d > 2.0) .map(d -> d * 10) .distinct() .sorted();
filter keeps only values that satisfy the predicate. map applies a DoubleUnaryOperator to each element. distinct removes duplicates, and sorted orders the remaining values in ascending order.
Unlike Stream<Double>, the primitive stream does not accept a general Function<T, R> for map. The operation takes a DoubleUnaryOperator, a functional interface with a double applyAsDouble(double) method. This avoids boxing at every stage of the pipeline.
boxed() converts a DoubleStream into a Stream<Double>. This is occasionally necessary when you need to use a collector that only works with object streams, such as Collectors.toList().
List<Double> list = DoubleStream.of(1.0, 2.0, 3.0) .boxed() .collect(Collectors.toList());
Terminal Operations and Their Return Types
Terminal operations trigger pipeline execution. DoubleStream provides several that return primitive results directly.
double sum = DoubleStream.of(1.0, 2.0, 3.0).sum(); OptionalDouble average = DoubleStream.of(1.0, 2.0, 3.0).average(); OptionalDouble max = DoubleStream.of(1.0, 2.0, 3.0).max(); long count = DoubleStream.of(1.0, 2.0, 3.0).count();
sum returns a primitive double. average and max return OptionalDouble because the stream may be empty. Calling getAsDouble() on an empty OptionalDouble throws NoSuchElementException, so check isPresent() first or use orElse.
summaryStatistics() bundles several results into a single object:
DoubleSummaryStatistics stats = DoubleStream.of(1.0, 2.0, 3.0) .summaryStatistics(); double min = stats.getMin(); double max = stats.getMax(); double avg = stats.getAverage(); long count = stats.getCount();
This is the most efficient way to compute multiple aggregates in one pass, because the stream is consumed only once.
forEach and forEachOrdered are terminal operations for side effects. Use forEachOrdered when the order of processing matters for parallel streams.
Performance: Why Primitive Streams Avoid Boxing
The main reason to choose DoubleStream over Stream<Double> is allocation. Each element in a Stream<Double> is a reference to a Double object. When a pipeline processes millions of values, boxing creates a large number of short-lived objects that the garbage collector must reclaim.
DoubleStream keeps values on the stack or in primitive arrays, so no per-element allocation occurs. The difference is most visible in tight loops over large arrays. For small datasets, the overhead is negligible, and readability may matter more than allocation cost.
There is a tradeoff. DoubleStream supports a narrower set of operations than Stream<Double>. You cannot apply arbitrary Function instances to elements without converting back to an object stream. If your pipeline relies heavily on custom object transformations, the boxing cost may be unavoidable.
Edge Cases: NaN, Infinity, and Empty Streams
Floating-point semantics introduce behavior that differs from integer streams.
sum() follows IEEE 754 rules. If the stream contains Double.NaN, the result is NaN. If it contains both positive and negative infinity, the result is NaN as well. min() and max() use the natural ordering of double values, which treats -0.0 as less than 0.0.
An empty DoubleStream produces OptionalDouble.empty() for average, min, and max. sum() returns 0.0 for an empty stream, and count() returns 0.
OptionalDouble result = DoubleStream.empty().average(); System.out.println(result.isPresent()); // false
DoubleStream.empty() is the standard way to represent a stream with no elements. It is useful when a method must return a DoubleStream but has no data to provide.
DoubleStream vs Stream<Double>: Choosing the Right Type
The choice between the two types depends on the source data and the operations required.
| Consideration | DoubleStream | Stream<Double> |
|---|---|---|
| Element storage | Primitive double | Boxed Double reference |
| Per-element allocation | None | One object per element |
| Available operations | Primitive-specific set | Full Stream API |
| Conversion | boxed() to object stream | mapToDouble() to primitive |
Use DoubleStream when the data originates from a double[] or a computation that produces primitives, and when the pipeline consists of filtering, mapping, and aggregation. Use Stream<Double> when you need collectors, custom Function transformations, or interoperability with generic APIs that require object types.
A common pattern is to convert a Stream<Double> to DoubleStream early in the pipeline, perform the heavy numeric work, and convert back only at the end if necessary. This keeps the allocation-heavy portion of the pipeline as small as possible while still allowing object-stream operations where they are genuinely required.