Learning outcome: Build stream pipelines with filtering, mapping, sorting, and reduction; distinguish intermediate and terminal operations; and combine UTF-8 file input with validated data processing.
A java.util.stream.Stream<T> describes a computation over elements. It does not store elements like a list and is distinct from an I/O stream. A collection, array, generator, or file can supply its source.
Source, Intermediate Operations, and Terminal Operation
Intermediate operations describe transformations and return another stream. They are lazy: traversal is driven by a terminal operation. The implementation may optimize away work that does not affect the result, so do not rely on side effects in intermediate operations. A terminal operation consumes the pipeline; obtain a fresh stream for another traversal. See the stream package contract.
| Operation | Kind | Purpose |
|---|---|---|
filter(predicate) |
Intermediate | Keep matching elements |
map(function) |
Intermediate | Transform each element |
flatMap(function) |
Intermediate | Flatten streams produced from elements |
distinct() |
Intermediate | Remove duplicates using equality |
sorted() / sorted(comparator) |
Intermediate | Order elements |
limit(n) |
Intermediate | Keep at most n elements |
collect(collector) |
Terminal | Build a result, such as a list or grouped map |
reduce(...) |
Terminal | Combine elements into a value |
count() |
Terminal | Count elements |
forEach(action) |
Terminal | Perform an action for each element |
anyMatch(predicate) |
Terminal, short-circuiting | Test whether an element matches |
findFirst() |
Terminal, short-circuiting | Return the first element, if present |
filter can reduce the number of elements. map changes each element into one result. For example, map(String::length) turns names into lengths; it does not remove short names. A short-circuiting operation may produce its answer without reading the entire source.
Filter, Transform, Sort, and Collect
Save as StreamMarksDemo.java:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamMarksDemo {
public static void main(String[] args) {
List<Integer> marks = Arrays.asList(35, 72, 90, 72, 48);
List<Integer> revised = marks.stream()
.filter(mark -> mark >= 40)
.map(mark -> Math.min(mark + 5, 100))
.distinct()
.sorted()
.collect(Collectors.toList());
int total = marks.stream().reduce(0, Integer::sum);
double average = marks.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println("Revised: " + revised);
System.out.println("Original: " + marks);
System.out.println("Total: " + total);
System.out.println("Average: " + average);
}
}
Output:
Revised: [53, 77, 95]
Original: [35, 72, 90, 72, 48]
Total: 317
Average: 63.4
Trace the first pipeline: filtering yields 72, 90, 72, 48; adding five yields 77, 95, 77, 53; removing duplicates yields 77, 95, 53; sorting yields 53, 77, 95. Removing duplicates is appropriate for this demonstration of distinct score values, but would lose student records if one entry represented one student's result.
Each calculation starts with a fresh marks.stream(). The original list is unchanged because these operations only read its values and create results. A lambda can still mutate a mutable source object if written to do so; streams do not enforce deep immutability.
Reduction and Primitive Streams
reduce(0, Integer::sum) combines elements using addition, with zero as the identity. For an empty stream, this overload returns zero. The reduction function should be associative and compatible with the identity, especially when work may be partitioned. Subtraction is not associative, and using it in a parallel reduction can change the result.
IntStream, LongStream, and DoubleStream support primitive processing. mapToInt avoids boxed values for subsequent numeric operations and provides sum, min, max, and average. Operations such as average have no numeric result on an empty stream, so they return an optional value. Choosing orElse(0.0) is an application policy; sometimes reporting "no marks" is more accurate.
Collectors.toList() is used here for Java 8 compatibility; its contract does not guarantee a particular list implementation or mutability. If an ArrayList is specifically required, use Collectors.toCollection(ArrayList::new). See the Collectors API.
Integrated Example: Read and Summarize Marks
This program connects the chapter's wrapper parsing, file I/O, lambdas, and method references. Create input-marks.txt in the working directory with the following content:
35
72
90
48
Save as FileMarksSummary.java:
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.IntSummaryStatistics;
import java.util.stream.Stream;
public class FileMarksSummary {
static int parseMark(String text) {
int mark = Integer.parseInt(text);
if (mark < 0 || mark > 100) {
throw new IllegalArgumentException("Mark out of range: " + mark);
}
return mark;
}
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(
Paths.get("input-marks.txt"), StandardCharsets.UTF_8)) {
IntSummaryStatistics summary = lines
.map(String::trim)
.filter(line -> !line.isEmpty())
.mapToInt(FileMarksSummary::parseMark)
.filter(mark -> mark >= 40)
.summaryStatistics();
System.out.println("Pass count: " + summary.getCount());
if (summary.getCount() > 0) {
System.out.println("Highest pass mark: " + summary.getMax());
System.out.println("Pass average: " + summary.getAverage());
} else {
System.out.println("No passing marks");
}
} catch (IOException | UncheckedIOException e) {
System.err.println("Could not read marks: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.err.println("Invalid mark: " + e.getMessage());
}
}
}
Output:
Pass count: 3
Highest pass mark: 90
Pass average: 70.0
Blank lines are ignored. Every nonblank mark is parsed and range-checked before the pass filter. Thus, invalid values are reported rather than silently dropped as failures. NumberFormatException extends IllegalArgumentException, so the final catch also handles nonnumeric input. An error stops the calculation and no partial summary is printed.
The stream returned by Files.lines holds an open file and must be closed. A read error may occur while the terminal operation traverses the file, not just when opening it; later I/O errors are wrapped in UncheckedIOException. See Files.lines.
Ordering, Side Effects, and Parallel Execution
stream() creates a sequential stream. parallelStream() or parallel() allows partitioned execution but does not guarantee faster results. Small datasets and I/O-heavy operations may gain nothing from parallelism.
Avoid changing the source while a pipeline runs or updating a shared ArrayList inside a parallel forEach. Prefer a collector or reduction. Parallel forEach does not guarantee encounter order; use forEachOrdered when the source has an encounter order that must be respected. Stateful operations such as sorting can require substantial buffering even though streams are lazy.
In simple terms: A stream pipeline describes what to keep, how to change it, and what result to produce. The terminal operation asks Java to perform the work. A second question needs a fresh pipeline.
Practice
- Return passing marks in descending order without changing the source list.
- Count passing students without removing repeated marks.
- Group names by their length using
Collectors.groupingBy(String::length). - Extend the file program to report the overall average as well as the pass average.
- Test the file program with blank input, no passes, invalid text, and a mark above 100.
Quick Check
1. Does filter alone execute a complete pipeline?
No. It adds an intermediate stage. A terminal operation drives traversal.
2. Can the same stream be used for count and then collect?
No. A stream is single-use. Obtain a fresh stream from the source for the second terminal operation.
3. Why does the example validate before filtering passing marks?
Otherwise a value such as -5 could be silently discarded as a failing mark, hiding invalid input.
4. Must a stream created from a list normally be closed?
No external resource normally needs closing. Streams backed by resources such as Files.lines must be closed.
Chapter 2.2 Review
Design a student results tool that parses marks into a List<Integer>, writes and reads a UTF-8 report, serializes trusted student records, and creates independent copies of mutable marks arrays. Use predicates to validate marks, method references for existing operations, and streams to compute result summaries. Explain which steps represent data, which move data, and which process data.
The chapter's central distinctions are primitive versus wrapper, bytes versus characters, serialization versus copying, and I/O streams versus Stream API pipelines. Apply each mechanism where its contract fits the task.