The Java Stream API processes a sequence of elements through a pipeline. A source supplies the elements, intermediate operations describe transformations, and a terminal operation produces a result. For example, you can filter a list of prices, convert cents to labels, and collect those labels without manually maintaining a destination list. A stream is not another collection: it does not store your data and it can be consumed only once.
Build a pipeline from source to result
Calling stream on a collection creates a sequential stream. filter keeps elements accepted by a Predicate; map converts each accepted element using a Function. Neither call alone processes the complete input. A terminal operation such as toList, count, or reduce starts traversal. This lazy behavior lets pipelines avoid unnecessary work, particularly with operations such as findFirst. Write each stage according to its purpose instead of adding transformations merely to shorten a loop.
Distinguish map from filter and flatMap
filter preserves element types while selecting a subset. map keeps one result for each incoming element, even when that result has a different type. flatMap is useful when one input represents several results, such as a group containing several members; it flattens the nested streams into a single sequence. These operations solve different problems. A mapping function that sometimes returns null still creates a null result unless you explicitly filter or otherwise handle it.
Reduce values using an identity
reduce combines elements into one value. For addition, zero is the identity because adding zero leaves the other value unchanged. The accumulator Integer::sum combines the running value with each element. The combination should be associative if you expect reliable parallel behavior. Subtraction is not associative, so replacing sum with subtraction is not a sound general reduction. Primitive streams offer specialized sum, average, and summaryStatistics operations that are often clearer for numeric work.
Collect lists and grouped results
Collectors.groupingBy classifies elements and builds a map from keys to groups. A downstream collector such as counting replaces each group list with a count. HashMap iteration order is not specified, so the example prints selected keys rather than depending on map formatting. Stream.toList returns an unmodifiable list on Java 17. If later editing is required, collect with Collectors.toCollection(ArrayList::new) or copy the result into a modifiable list explicitly.
Handle duplicate map keys deliberately
Collectors.toMap needs a plan when two elements produce the same key. The two-argument form throws IllegalStateException for duplicates. A merge function can keep an existing value, choose the new one, or combine them, according to the actual requirement. Do not silently discard records just to remove an exception. When ordering matters, a map supplier such as LinkedHashMap::new makes that intent explicit instead of relying on a default implementation's incidental order.
Practice tasks and checks
Modify PricePipeline to select prices strictly above twenty-eight and predict the resulting list before running it. Next, remove every matching price by using a threshold larger than all source elements; toList should produce an empty list rather than null. Add a second calculation using a fresh stream to count matching prices. Do not reuse the consumed stream from the first calculation. For CategoryCounts, ask for an absent category using getOrDefault with zero as a Long value. Finally, create a toMap collector for repeated categories and decide whether repeated values should be counted, rejected, or replaced. Document that choice before writing the merge function so duplicate handling follows a requirement rather than an exception workaround.
Java Summary
- Intermediate stream operations are lazy until a terminal operation runs.
- A consumed stream cannot be reused.
- Choose duplicate-key and result-mutability policies explicitly.
- Continue with collection contracts when these examples are clear.
Java Examples
Each example shows the code, the output it produces and what each part does.
Filter and transform prices
Save this program as PricePipeline.java. Compile it with javac PricePipeline.java and run it with java PricePipeline.
Expected output:
The filter rejects twelve and seven. map runs only for the remaining prices, producing two labels in their original encounter order. toList triggers the traversal and gathers the results. Printing prices afterward shows that the pipeline did not alter the source list. The result is unmodifiable, so labels.add would throw UnsupportedOperationException. This property describes the returned list, not whether its elements would be mutable if they were objects.
import java.util.List;
public class PricePipeline {
public static void main(String[] args) {
List<Integer> prices = List.of(12, 28, 7, 35);
List<String> labels = prices.stream()
.filter(price -> price >= 20)
.map(price -> "Price " + price)
.toList();
System.out.println(labels);
System.out.println(prices);
}
}[Price 28, Price 35]
[12, 28, 7, 35]Sum values with a reduction
Save this program as PointReduction.java. Compile it with javac PointReduction.java and run it with java PointReduction.
Expected output:
The first reduction begins with zero, adds nine, then four, then eleven, yielding twenty-four. An empty stream contributes no elements, so it returns the identity zero. Using an identity makes the result an int rather than an Optional<Integer>. If you need to distinguish an empty collection from a legitimate zero total, choose a different return contract or use the identity-free reduction and handle its Optional result.
import java.util.List;
public class PointReduction {
public static void main(String[] args) {
List<Integer> points = List.of(9, 4, 11);
int total = points.stream().reduce(0, Integer::sum);
int emptyTotal = List.<Integer>of().stream().reduce(0, Integer::sum);
System.out.println(total);
System.out.println(emptyTotal);
}
}24
0Count groups with a downstream collector
Save this program as CategoryCounts.java. Compile it with javac CategoryCounts.java and run it with java CategoryCounts.
Expected output:
The classifier uses each category string as its group key. counting counts elements in each group and returns Long values, so book maps to two and game maps to one. Selecting keys gives deterministic output despite unspecified map iteration order. A missing key would return null from get; use getOrDefault when a zero count is the intended meaning for categories absent from the source.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CategoryCounts {
public static void main(String[] args) {
List<String> categories = List.of("book", "game", "book", "map");
Map<String, Long> counts = categories.stream()
.collect(Collectors.groupingBy(
category -> category, Collectors.counting()));
System.out.println(counts.get("book"));
System.out.println(counts.get("game"));
}
}2
1Java FAQ
Can I reuse the same stream?
No. After a terminal operation has consumed it, another terminal operation normally throws IllegalStateException. Obtain a new stream from the collection for another calculation. Reusing the collection is fine; reusing that consumed traversal object is not.
Is forEach the same as map?
No. map transforms elements and returns a stream for further processing. forEach is a terminal action returning void. Use map followed by collect when building a result, and reserve forEach for deliberate actions such as displaying values.
How do I sum a field of objects with a stream?
Map to a primitive stream and sum it: orders.stream().mapToInt(Order::quantity).sum(). For a total per group, use a downstream collector such as Collectors.groupingBy(Order::customer, Collectors.summingInt(Order::quantity)).
How do I join strings with a stream?
Use Collectors.joining. names.stream().collect(Collectors.joining(", ")) gives "Ana, Ben, Cal", and the three-argument form adds a prefix and suffix, such as joining(", ", "[", "]"). For a list that's already built, String.join(", ", names) is shorter.
What does Collectors.partitioningBy do?
It splits elements into two groups using a predicate and returns a Map<Boolean, List<T>>, with true holding the matches and false the rest. Both keys are always present, even when a group is empty, which makes it handy for pass and fail reports.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which operation starts traversal in PricePipeline?
What does reducing an empty integer stream with zero and Integer::sum return?
Java Quiz
2 questions to check what you learned.
Which collector counts elements for each classification key?
- groupingBy with counting
- joining
- toList alone
Why should you avoid modifying the source in a pipeline?
- Traversal requires a stable source contract
- Streams always copy every source
- Modification automatically sorts the source