Stream API
The Stream API (Java 8+) enables functional-style processing of collections and arrays. It allows you to transform, filter, aggregate, and process data declaratively with high readability and optional parallelism.
The main reason the Stream API became so important is that Java programs often spend a large amount of time processing groups of values. Developers filter lists, convert objects into other forms, remove duplicates, sort data, count matching records, calculate totals, group values, and create reports. Before Java 8, this was usually done with explicit loops and temporary collections. That style works, but it can hide the purpose of the code behind indexing, iteration variables, and manual result construction.
Streams give Java a declarative way to describe data processing. Instead of writing every step of how to iterate, you describe what should happen to the data. A stream pipeline can say: take this list, keep only active records, convert each record to a display value, sort the result, and collect it into a list. The code reads closer to the business intention, while the Stream API manages the traversal and execution.
This does not mean streams replace every loop. Loops are still useful for complex control flow, early mutation-heavy algorithms, and cases where step-by-step logic is clearer. Streams are strongest when the task is data transformation, filtering, aggregation, or querying. Used well, they reduce boilerplate and make Java code easier to scan. Used poorly, with long lambdas and hidden side effects, they can become harder to debug than a normal loop.
This is a very high-frequency interview topic and heavily used in modern Java applications.
Interviewers ask about streams because the topic connects lambdas, functional interfaces, collections,
lazy evaluation, terminal operations, Optional, collectors, and parallel processing. A strong
answer should explain that a stream is not a collection and does not store data. It is a processing view
over a source. You should also be able to explain the pipeline model: source, intermediate operations, and
terminal operation.
What Is a Stream?
A stream is:
- A sequence of elements
- From a data source (Collection, Array, I/O)
- Supporting functional operations
- Not a data structure
- Does not modify the source
list.stream();
A stream represents a sequence of elements that can be processed through a set of operations. The source
can be a collection, an array, generated values, a file, or another data-producing API. The stream itself
does not store those elements. It works with the source and describes a chain of processing steps. This is
why a stream is different from a List, Set, or Map.
A collection is about storing and organizing data. A stream is about processing data. When you call
list.stream(), the list remains the data source. The stream provides a way to filter, map,
sort, aggregate, or consume the elements of that list. In most stream operations, the original source is not
modified. Instead, the pipeline produces a result, such as a new list, a count, an optional value, or a
side-effect action.
Streams are closely tied to functional interfaces. filter expects a Predicate,
map expects a Function, and forEach expects a
Consumer. Lambdas and method references provide these behaviors concisely. The Stream API
controls the flow of elements, while the functional interface implementations define the rules for each
step.
Why Stream API Was Introduced
- Reduce boilerplate code
- Enable functional programming
- Improve readability and maintainability
- Support easy parallelism
- Separate what from how
The Stream API was introduced to make common data processing tasks more expressive. A loop tells Java how
to walk through values one by one. A stream pipeline tells Java what result is needed. For example, instead
of writing a loop that checks each value, adds matching values to a temporary list, and then loops again to
transform them, a stream can chain filter, map, and collect in one
readable sequence.
Streams also support internal iteration. With external iteration, your code controls the loop. With internal iteration, the library controls iteration and your code supplies behavior. Internal iteration gives the Stream API more flexibility to optimize execution, short-circuit operations, or execute work in parallel when appropriate. This is a major reason streams feel different from traditional loops.
Another goal was to bring functional programming ideas into Java without making Java a purely functional language. Streams encourage operations that transform values rather than mutate shared state. They work best with short lambdas, method references, immutable results, and clear collection of output. This style can improve maintainability when processing data-heavy code.
Stream Pipeline (Core Concept)
A stream operation follows this pipeline:
Source → Intermediate Operations → Terminal Operation
Example:
list.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.forEach(System.out::println);
The pipeline is the core mental model for streams. The source begins the pipeline. Intermediate operations describe transformations or filtering rules. The terminal operation starts execution and produces the final result or side effect. Without a terminal operation, the stream pipeline is only a description. Nothing is actually processed.
In the example, list.stream() creates the stream source. filter keeps only values
greater than 10. map transforms each remaining value by multiplying it by 2.
forEach is the terminal operation that consumes the values and prints them. The pipeline reads
from left to right as a sequence of data processing decisions.
Stream pipelines are easier to understand when each stage has one clear responsibility. filter
should test whether an element should remain. map should convert one value into another.
sorted should define ordering. collect should build the result. When these
responsibilities are mixed together, for example by mutating external state inside map, the
stream becomes harder to reason about.
Characteristics of Streams
- No storage – operates on source
- Lazy evaluation – executes only on terminal operation
- One-time use – cannot be reused
- Functional – operations do not mutate source
The no-storage characteristic means a stream does not own the data. It reads from a source and processes elements as needed. This is why creating a stream is lightweight compared with copying an entire collection. However, the source must still exist and must be safe to read while the stream is processed.
Lazy evaluation is one of the most important stream behaviors. Intermediate operations such as
filter and map do not execute immediately. They are stored as part of the
pipeline. Execution begins only when a terminal operation such as collect, count,
reduce, or forEach is called. This laziness allows streams to avoid unnecessary
work and enables short-circuit operations such as findFirst or anyMatch.
Streams are one-time-use objects. After a terminal operation consumes a stream, it cannot be reused. If you need to process the same source again, create a new stream from the source. This often surprises beginners because collections can be iterated many times, but a stream represents a single traversal pipeline.
Creating Streams
From Collection
Streams = list.stream();
From Array
Streams = Arrays.stream(arr);
Using Stream.of()
Streams = Stream.of(1, 2, 3);
Streams can be created from many sources. Collections are the most common source because
Collection provides the stream() and parallelStream() methods.
Arrays can be converted using Arrays.stream. Individual values can be turned into a stream
with Stream.of. Files and other APIs can also provide streams for line-by-line or generated
processing.
Choosing the right source matters because it affects ordering, performance, and possible parallel behavior. A list has a predictable encounter order. A set may not preserve insertion order unless it is a specific ordered set implementation. A generated stream may be infinite and must be limited. A file-backed stream may require closing resources. Streams are convenient, but the source still influences how the pipeline behaves.
Intermediate Operations (Lazy)
Return a new Stream.
Intermediate operations transform a stream into another stream. They do not produce the final answer by
themselves. Because they are lazy, Java can combine them efficiently during terminal execution. For example,
a pipeline with filter followed by map does not necessarily create a temporary
collection between those two steps. Elements flow through the pipeline as needed.
Intermediate operations are usually divided into stateless and stateful operations. Stateless operations,
such as filter and map, can process each element independently. Stateful
operations, such as distinct and sorted, may need to remember information about
other elements before producing output. Stateful operations can be more expensive, especially for large or
parallel streams.
Common Intermediate Operations
| Method | Purpose |
|---|---|
| filter() | Condition-based filtering |
| map() | Transform elements |
| flatMap() | Flatten nested streams |
| distinct() | Remove duplicates |
| sorted() | Sort elements |
| limit() | Limit size |
| skip() | Skip elements |
| peek() | Debugging |
filter()
list.stream()
.filter(x -> x % 2 == 0)
.forEach(System.out::println);
map()
list.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
flatMap() (Interview Favorite)
List> data = List.of(List.of(1,2), List.of(3,4)); data.stream() .flatMap(List::stream) .forEach(System.out::println);
Converts nested structure into flat stream.
filter is used when you want to select only elements that match a condition. The condition is
represented by a Predicate. map is used when each input element should become
another value. The transformation is represented by a Function. These two operations are the
foundation of many stream pipelines because most data processing involves selecting and transforming.
flatMap is useful when each element produces another stream or collection, and the final result
should be flattened into a single stream. For example, a list of orders may contain a list of order items.
If you want one stream of all order items across all orders, flatMap is the correct operation.
This is why it is a favorite interview topic: it tests whether you understand the difference between
transforming values and flattening nested structures.
peek should mainly be used for debugging or observing values as they move through a pipeline.
It is not a good replacement for map or forEach. If you rely on
peek for important business behavior, the stream can become confusing because
peek is still an intermediate operation and depends on a later terminal operation to execute.
Terminal Operations (Trigger Execution)
Consume the stream and produce a result or side effect.
Terminal operations are the operations that make a stream pipeline run. They either produce a final value, such as a count, collection, optional, or reduced result, or they perform a side effect such as printing each element. After a terminal operation completes, the stream is consumed and cannot be used again.
Common Terminal Operations
| Method | Result |
|---|---|
| forEach() | Iteration |
| collect() | Collection |
| reduce() | Single value |
| count() | Long |
| anyMatch() | boolean |
| allMatch() | boolean |
| noneMatch() | boolean |
| findFirst() | Optional |
| findAny() | Optional |
collect()
Listeven = list.stream() .filter(x -> x % 2 == 0) .collect(Collectors.toList());
reduce() (Interview Favorite)
int sum =
list.stream()
.reduce(0, Integer::sum);
collect is the most common terminal operation when you want to build a result container. It is
often used with Collectors.toList(), Collectors.toSet(),
Collectors.groupingBy(), Collectors.partitioningBy(), or
Collectors.joining(). Collectors are especially useful because they express aggregation in a
standard way instead of forcing every developer to write manual accumulation logic.
reduce is used when many elements should be combined into a single result. Summing numbers,
multiplying values, finding a combined score, or merging strings are common examples. A reduce operation
needs a clear identity value and a combining function. In interviews, explain that reduce is about
aggregation, while collect is usually about building a mutable result container through a collector.
Matching and finding operations can short-circuit. anyMatch can stop when it finds the first
matching element. allMatch can stop when it finds an element that fails the condition.
findFirst can return the first available element based on encounter order. This is another
benefit of lazy execution: the stream may not need to process every element when the terminal operation can
answer early.
Stream vs Collection (Interview Table)
| Aspect | Stream | Collection |
|---|---|---|
| Storage | ❌ No | ✔ Yes |
| Iteration | Internal | External |
| Reusability | ❌ No | ✔ Yes |
| Laziness | ✔ Yes | ❌ No |
| Parallelism | ✔ Easy | ❌ Manual |
A collection and a stream are often used together, but they are not the same abstraction. A collection is a data structure. It stores elements and can be reused. A stream is a processing pipeline over a data source. It is consumed once by a terminal operation. This distinction is one of the most common interview questions because it checks whether you understand streams beyond syntax.
Collections use external iteration when you write loops over them. Streams use internal iteration, where the Stream API controls traversal and your code provides functional behavior. Internal iteration makes stream pipelines expressive and enables optimizations, but it also means you should avoid depending on external mutable state inside stream operations.
Sequential vs Parallel Streams
Sequential (Default)
list.stream();
Parallel Stream
list.parallelStream();
- Automatic multi-threading
- Not always faster
- Order may not be preserved
Sequential streams process elements in a single logical sequence and are the default choice. Parallel streams divide work across multiple threads using the common fork-join pool. This can improve performance for large, CPU-heavy, independent operations, but it can also make code slower if the workload is small, blocking, order-sensitive, or dependent on shared mutable state.
Parallel streams should not be used just because they are easy to write. Parallel execution has overhead. Work must be split, scheduled, and combined. If each operation is cheap, that overhead may cost more than it saves. Parallel streams also require thread-safe behavior. Updating a shared list, counter, or object from inside a parallel stream is a common source of bugs.
Ordering is another consideration. Some operations preserve encounter order, while others may not guarantee the order you expect when parallelized. If order matters, use ordered sources and operations carefully, or prefer sequential streams. In production code, performance should be measured before replacing a sequential stream with a parallel stream.
Stateless vs Stateful Operations
- Stateless: map, filter (preferred)
- Stateful: distinct, sorted (costly)
Stateless operations can process each element independently. A filter condition only needs the
current element. A map transformation usually only needs the current element. These operations
are simple, predictable, and suitable for many pipelines.
Stateful operations require information about other elements. sorted must see enough elements
to determine ordering. distinct must remember which values have already appeared. These
operations are useful, but they can be more expensive in memory and time, especially with large datasets or
parallel streams. Understanding this distinction helps explain why not every stream pipeline has the same
performance characteristics.
Optional with Streams
Optionalmax = list.stream().max(Integer::compareTo);
Avoids NullPointerException.
Some stream operations may or may not find a value. For example, findFirst, findAny,
min, and max return Optional because the stream might be empty.
Instead of returning null, Java uses Optional to force the caller to handle the
absence of a value explicitly.
This is safer than assuming a value always exists. You can use methods such as orElse,
orElseGet, ifPresent, and orElseThrow depending on what should
happen when the stream result is empty. In interviews, mention that Optional reduces
null-related mistakes but should still be handled thoughtfully.
Common Beginner Mistakes
- Modifying source inside stream
- Reusing a stream
- Using forEach() instead of map()
- Overusing parallel streams
- Writing complex logic in lambdas
The biggest beginner mistake is treating streams like loops with different syntax. A stream pipeline should
describe data processing. If the code uses forEach everywhere, mutates external collections,
and ignores return values, it may be better as a normal loop. map should be used for
transformation, filter for selection, and collect for building results.
Reusing a stream is another common trap. Once a terminal operation has run, the stream is closed for
further processing. Create a new stream from the source if another pipeline is needed. This is different
from reusing a collection, and understanding this rule prevents IllegalStateException errors.
Overusing parallel streams is a production risk. Parallel execution can be useful, but it is not a default optimization. If the stream operation performs I/O, depends on order, uses shared state, or processes a small amount of data, a parallel stream may hurt correctness or performance. Measure before assuming it is faster.
Best Practices (Production-Grade)
- Keep lambdas small and readable
- Prefer method references
- Use streams for data transformation, not business logic
- Be cautious with parallel streams
- Prefer Collectors for aggregation
Production-grade stream code should be readable first. Each pipeline stage should have a clear purpose. If a lambda is too large to understand quickly, extract it into a named method. If the stream pipeline becomes too long, consider splitting it into meaningful intermediate variables or using ordinary code. Streams are a tool for clarity, not a requirement for every data-processing task.
Prefer method references when they make intent obvious. String::toUpperCase,
System.out::println, and Integer::sum are concise because they point to known
operations. However, do not force method references when a lambda explains the rule better. The best choice
is the one that makes the code easiest to read.
Use collectors for aggregation instead of manually mutating external collections inside
forEach. Collectors are designed for stream result construction and work better with the stream
model. For grouping, partitioning, joining, summarizing, or collecting to lists and sets, the
Collectors utility provides standard, expressive solutions.
Interview-Ready Answers
Short Answer
Stream API provides a functional way to process collections in Java.
Detailed Answer
In Java, the Stream API enables functional-style operations on sequences of elements. Streams support lazy evaluation, internal iteration, and can be processed sequentially or in parallel using intermediate and terminal operations without modifying the underlying data source.
A stronger interview answer should mention that a stream pipeline begins with a source, applies zero or more intermediate operations, and ends with one terminal operation. Intermediate operations are lazy and return streams. Terminal operations trigger execution and produce a result or side effect. Streams are consumed once and should not be confused with collections because streams do not store data.
If asked for examples, use filter for selecting values, map for transforming
values, collect for building a result collection, reduce for aggregation,
findFirst for optional retrieval, and parallelStream for cautious parallel
processing. These examples show both practical usage and conceptual understanding.
Key Takeaway
Streams are about data processing, not data storage. They make Java code cleaner, declarative, and scalable, especially when combined with lambdas and functional interfaces.
The most important mindset is to view a stream as a data-processing pipeline. The source supplies elements, intermediate operations describe what should happen to those elements, and the terminal operation produces the final result. This model keeps code focused on transformation and aggregation rather than manual iteration details.
To use streams well, keep operations small, avoid shared mutable state, understand laziness, choose the right terminal operation, and use parallel streams only when the workload is suitable. When applied with these rules, the Stream API becomes one of the most useful tools in modern Java.
Real-World Stream API Usage
In real applications, streams are often used to prepare data for display, reports, exports, and API responses. For example, an application may load a list of orders, filter only completed orders, map each order to a summary object, sort the summaries by date, and collect them into a response list. The stream pipeline expresses this as a sequence of meaningful transformations instead of several manual loops and temporary variables.
Streams are also useful in testing and automation code. A test report utility may filter failed test cases, group defects by severity, count skipped scenarios, join log messages, or extract unique browser names from execution results. These are natural stream operations because they involve selecting, transforming, grouping, and aggregating data. The same skills used in application code apply directly to testing tools.
Business rules should still be designed carefully. If a stream pipeline contains complex domain decisions, move those decisions into named methods or services and call them from the pipeline. This keeps the stream readable while preserving proper separation of responsibility. The stream should describe the data flow, not hide an entire business process inside nested lambdas.
Collectors and Aggregation in Streams
Collectors are one of the most practical parts of the Stream API because many pipelines need to produce a
final container or summary. A simple Collectors.toList() creates a list from the processed
stream. Collectors.toSet() removes duplicate values according to set behavior.
Collectors.joining() builds a string from text values. These collectors replace common manual
patterns where developers create a result variable, loop over input, and add values one by one.
More advanced collectors express reporting-style logic clearly. groupingBy creates groups
based on a classifier function, such as grouping employees by department, defects by severity, or test
cases by automation status. partitioningBy splits data into two groups based on a boolean
condition, such as passed and failed tests or active and inactive users. These operations are common in
dashboards, summaries, and analytics-style features.
Collectors can also be combined. A grouped result can count items in each group, collect nested lists, calculate sums, or create summary statistics. This is powerful, but readability should guide the design. If a collector expression becomes difficult to read, break the logic into named helper methods or intermediate steps. Stream aggregation should make reporting logic clearer, not turn it into a dense expression that only the original author understands.
Primitive Streams and Performance
Java provides primitive streams such as IntStream, LongStream, and
DoubleStream to avoid unnecessary boxing and unboxing. A regular Stream<Integer>
works with wrapper objects, while IntStream works directly with primitive int
values. For small datasets this difference may not matter, but for large numeric processing it can improve
performance and reduce memory overhead.
Primitive streams also provide convenient numeric operations such as sum, average,
min, max, and summaryStatistics. These methods make numeric
aggregation direct. For example, if you have a stream of strings and want the total length, using
mapToInt(String::length).sum() expresses the operation clearly and avoids creating wrapper
objects for each length value.
The decision to use primitive streams should be practical. If the operation is naturally numeric, use the primitive stream form. If the operation is object-oriented and the numeric value is only one part of the logic, a regular object stream may be clearer. Performance matters, but maintainability still matters in most application code.
Debugging Stream Pipelines
Debugging streams can feel different from debugging loops because intermediate operations are lazy. If you
place a print statement inside a filter or map lambda but never call a terminal
operation, nothing happens. This behavior is correct. The stream has not been executed yet. When debugging,
first confirm that the pipeline has a terminal operation.
The peek method can help inspect values as they flow through a pipeline, but it should be used
mainly for temporary debugging. For example, you can place peek after filter to
see which values survived the condition, or after map to see transformed values. Once the issue
is understood, remove unnecessary peek calls so the pipeline remains clean.
Another debugging technique is to split a long pipeline into smaller named steps. Store the filtered result, transformed result, or grouped result in a meaningful variable when investigating behavior. Although streams encourage chaining, there is no rule that every pipeline must be one long expression. Shorter, named steps can make complex data processing easier to review and test.
When Not to Use Streams
Streams are not the best choice for every problem. If the logic depends heavily on indexes, multiple mutable variables, nested control flow, or complex early exits, a traditional loop may be clearer. A loop can also be easier when each step modifies several related pieces of state or when the algorithm is not naturally a sequence of transformations.
Streams can also be a poor fit when the code is written only for side effects. If a pipeline exists mainly to update external objects, append to shared collections, or trigger several unrelated actions, the functional style becomes misleading. In those cases, explicit control flow may communicate intent better. The goal is not to use streams everywhere; the goal is to write code that is correct, readable, and easy to maintain.
A practical rule is to use streams when the operation can be described as selecting, transforming, sorting, grouping, reducing, or collecting data. Use loops when the operation is procedural, stateful, or algorithmic in a way that streams would obscure. Mature Java developers are comfortable with both approaches and choose based on clarity.
Stream API Examples (1-25)
1. Creating a Stream from a Collection
import java.util.*;
class Demo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
list.stream().forEach(System.out::println);
}
}
2. filter() – Select Elements
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3, 4)
.stream()
.filter(x -> x % 2 == 0)
.forEach(System.out::println);
}
}
3. map() – Transform Elements
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList("a", "bb", "ccc")
.stream()
.map(String::length)
.forEach(System.out::println);
}
}
4. Chaining filter() + map()
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3, 4)
.stream()
.filter(x -> x > 2)
.map(x -> x * 10)
.forEach(System.out::println);
}
}
5. sorted() – Natural Order
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(3, 1, 2)
.stream()
.sorted()
.forEach(System.out::println);
}
}
6. sorted() with Comparator (Descending)
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(3, 1, 2)
.stream()
.sorted((a, b) -> b - a)
.forEach(System.out::println);
}
}
7. distinct() – Remove Duplicates
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 1, 2, 3, 3)
.stream()
.distinct()
.forEach(System.out::println);
}
}
8. limit() and skip()
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3, 4, 5)
.stream()
.skip(2)
.limit(2)
.forEach(System.out::println);
}
}
9. count() – Terminal Operation
import java.util.*;
class Demo {
public static void main(String[] args) {
long c = Arrays.asList("a", "bb", "ccc")
.stream()
.filter(s -> s.length() > 1)
.count();
System.out.println(c);
}
}
10. findFirst() and findAny()
import java.util.*;
class Demo {
public static void main(String[] args) {
System.out.println(
Arrays.asList(1, 2, 3).stream().findFirst().get()
);
}
}
11. anyMatch(), allMatch(), noneMatch()
import java.util.*;
class Demo {
public static void main(String[] args) {
boolean anyEven =
Arrays.asList(1, 3, 4).stream().anyMatch(x -> x % 2 == 0);
System.out.println(anyEven);
}
}
12. reduce() – Aggregate Values
import java.util.*;
class Demo {
public static void main(String[] args) {
int sum = Arrays.asList(1, 2, 3)
.stream()
.reduce(0, Integer::sum);
System.out.println(sum);
}
}
13. collect() to List
import java.util.*;
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
List<Integer> evens =
Arrays.asList(1, 2, 3, 4)
.stream()
.filter(x -> x % 2 == 0)
.collect(Collectors.toList());
System.out.println(evens);
}
}
14. collect() to Set
import java.util.*;
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
Set<Integer> set =
Arrays.asList(1, 1, 2)
.stream()
.collect(Collectors.toSet());
System.out.println(set);
}
}
15. Collectors.groupingBy()
import java.util.*;
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
Map<Integer, List<String>> map =
Arrays.asList("a", "bb", "ccc")
.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(map);
}
}
16. Collectors.partitioningBy()
import java.util.*;
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
Map<Boolean, List<Integer>> parts =
Arrays.asList(1, 2, 3, 4)
.stream()
.collect(Collectors.partitioningBy(x -> x % 2 == 0));
System.out.println(parts);
}
}
17. Collectors.joining()
import java.util.*;
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
String s =
Arrays.asList("Java", "Stream", "API")
.stream()
.collect(Collectors.joining(" "));
System.out.println(s);
}
}
18. Primitive Streams (IntStream)
import java.util.stream.*;
class Demo {
public static void main(String[] args) {
int sum = IntStream.range(1, 5).sum();
System.out.println(sum);
}
}
19. mapToInt() – Avoid Boxing
import java.util.*;
class Demo {
public static void main(String[] args) {
int total =
Arrays.asList("a", "bb", "ccc")
.stream()
.mapToInt(String::length)
.sum();
System.out.println(total);
}
}
20. Parallel Stream
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3, 4)
.parallelStream()
.forEach(System.out::println);
}
}
21. Sequential vs Parallel (When Order Matters)
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3)
.parallelStream()
.forEachOrdered(System.out::println);
}
}
22. Laziness of Streams (Interview Trap)
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3)
.stream()
.filter(x -> {
System.out.println("filter " + x);
return x > 1;
});
// ❌ No terminal operation → nothing executes
}
}
23. Stream Can Be Consumed Only Once
import java.util.*;
class Demo {
public static void main(String[] args) {
var s = Arrays.asList(1, 2).stream();
s.forEach(System.out::println);
// s.forEach(System.out::println); // ❌ IllegalStateException
}
}
24. peek() for Debugging
import java.util.*;
class Demo {
public static void main(String[] args) {
Arrays.asList(1, 2, 3)
.stream()
.peek(System.out::println)
.map(x -> x * 2)
.forEach(System.out::println);
}
}
25. Interview Summary – Stream API
source → intermediate ops → terminal op
Key Points
- Streams are lazy
- Intermediate ops return a new stream
- Terminal ops trigger execution
- Prefer primitive streams for performance
- Parallel streams need care with ordering & shared state