A stream represents a single-use computation over values, connecting intermediate operations to a terminal operation rather than storing another collection.
Java streams: lazy pipelines and bounded results
Execution starts at the terminal operation
filter and map describe work. A terminal operation such as collect, count, or sum triggers the pipeline. Building a stream does not eagerly produce a filtered copy of its source.
The program calculates accepted shipment weight from a bounded in-memory batch. It removes nonpositive weights and values above the intake limit, then sums the remaining ints as longs. It does not mutate the source list.
mapToLong avoids an object-valued arithmetic pipeline for the total. It does not remove the Integer objects already stored in the source list. A primitive stream and a primitive source solve related but different allocation questions.
Ordering and side effects need care
A stream can be consumed once. Calling a second terminal operation on the same stream instance is invalid. Obtain another stream from the source when another pass is required.
Side-effecting pipeline functions create dependencies that are difficult to reason about, especially under parallel execution. Keep predicates and mappings stateless and avoid modifying the source while traversal is running.
collect creates a result container; sorted may need to buffer values; distinct tracks seen values. Laziness does not mean every pipeline uses constant memory. A pipeline with an unbounded source needs a short-circuit or a finite termination condition.
Working program
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ShipmentWeightReport {
public static void main(String[] args) {
List<Integer> weights = Arrays.asList(12, -1, 8, 42, 5);
long acceptedTotal = weights.stream()
.filter(weight -> weight > 0 && weight <= 30)
.mapToLong(Integer::longValue)
.sum();
List<Integer> accepted = weights.stream()
.filter(weight -> weight > 0 && weight <= 30)
.collect(Collectors.toList());
System.out.println("total=" + acceptedTotal);
System.out.println(accepted);
}
}Output
total=25
[12, 8, 5]Cost and design choices
Each of the two pipelines makes one O(n) pass over the batch. The total uses a bounded accumulator, while the collected result needs O(k) storage for k accepted elements. Combining both outputs in one pass is possible, but a small clear batch may not justify a custom collector.
Parallel execution is a performance choice, not a guaranteed speed improvement. Splitting, coordination, ordering requirements, and contention can dominate small inputs. Blocking I/O inside a parallel pipeline is a separate scheduling concern.
Java 8 Collectors.toList does not guarantee a particular list type or its mutability. If a caller requires an ArrayList, use toCollection(ArrayList::new). If the result must be immutable, apply an explicit ownership boundary.
Connected lessons
Continue with Pipeline function contracts, Collected result storage, A possibly absent result.
Common Mistakes
- Do not reuse an already consumed stream.
- Do not use peek as the required location of a business side effect.
- Do not assume sorted, distinct, or collect needs only constant memory.
- Do not use parallelStream as an automatic fix for slow code.
Compare the Python boundary
Python generators: lazy iteration does not make retained output free.
