Java Streams
A Stream is a pipeline through which data elements flow, letting you perform operations like filtering, mapping, sorting, and aggregation without writing explicit loops. It was introduced in Java 8 (package java.util.stream).
In simple words:
Stream = Data Source → Intermediate Operations → Terminal Operation
Real-Life Example: Factory Conveyor Belt
Think of a bottling factory:
1Raw bottles → Reject cracked ones → Fill with juice → Put on labels → Pack into boxes2 (source) (filter) (map) (map) (terminal)- The machines (filter, fill, label) are set up along the belt, but nothing moves until the final packing station is switched on. This is lazy evaluation.
- Bottles pass through the stations one by one, not all at once.
- Once the batch is packed, the belt is finished. You cannot run the same batch through again. This is why a Stream cannot be reused.
- The original crate of raw bottles is not changed. The output is a new set of boxes.
Stream vs Collection
| Feature | Collection | Stream |
|---|---|---|
| Purpose | Stores data | Processes data |
| Data structure | Yes | No (it is a pipeline) |
| Modifies the source | Yes (add/remove) | No |
| Reusable | Yes | No (one terminal operation, then closed) |
| Evaluation | Eager | Lazy |
| Loop style | External iteration (you write loop) | Internal iteration (Stream does it) |
| Can be infinite | No | Yes (Stream.iterate, Stream.generate) |
Stream Pipeline
1Data Source2 ↓3Create Stream4 ↓5Intermediate Operations (zero or more, lazy)6 ↓7Terminal Operation (exactly one, triggers execution)8 ↓9Result| Step | Rules |
|---|---|
| Create | From a collection, array, values, builder, or generator |
| Intermediate | Zero or more, return another Stream, lazy, can be chained |
| Terminal | Exactly one, triggers execution, produces result, closes the Stream |
Example: Without Stream vs With Stream
Count salaries greater than 3000.
List<Integer> salaries = List.of(3000, 4000, 1000, 9000, 1000, 3500);// Without Streamint count = 0;for (int salary : salaries) {if (salary > 3000) {count++;}}System.out.println(count); // 3// With Streamlong total = salaries.stream().filter(salary -> salary > 3000).count();System.out.println(total); // 3
1salaries.stream() → filter() → count()2 (create) (intermediate) (terminal)The loop says how to do it. The Stream says what you want. This is the declarative style.
Ways to Create a Stream
| Source | Code |
|---|---|
| Collection | list.stream() |
| Array | Arrays.stream(arr) |
| Explicit values | Stream.of(1, 2, 3) |
| Builder | Stream.builder().add(1).add(2).build() |
| Iterate (infinite) | Stream.iterate(seed, fn) |
| Generate (infinite) | Stream.generate(supplier) |
| Range of ints | IntStream.range(1, 5), rangeClosed(1, 5) |
import java.util.*;import java.util.stream.*;// 1. From a collectionStream<Integer> s1 = List.of(1, 2, 3).stream();// 2. From an arrayint[] nums = {1, 2, 3, 4, 5};IntStream s2 = Arrays.stream(nums);String[] names = {"Akash", "Rahul", "Amit"};Stream<String> s3 = Arrays.stream(names);// 3. Stream.of()Stream<Integer> s4 = Stream.of(1, 2, 3, 4, 5);// 4. Stream.BuilderStream.Builder<Integer> builder = Stream.builder();builder.add(1);builder.add(2);builder.add(3);Stream<Integer> s5 = builder.build();// 5. Stream.iterate() with limitStream.iterate(1000, n -> n + 5000).limit(5).forEach(System.out::println);// 1000, 6000, 11000, 16000, 21000// 6. iterate with a stop condition (Java 9+)Stream.iterate(1, n -> n <= 20, n -> n * 2).forEach(System.out::println);// 1, 2, 4, 8, 16
Stream.iterate()andStream.generate()can produce an infinite Stream. Always restrict them withlimit()or a stop condition.
A Map has no stream() method. Stream its keySet(), values(), or entrySet() instead.
Intermediate Operations ⭐
Intermediate operations transform a Stream into another Stream. They are lazy: they do nothing until a terminal operation runs.
| Operation | Purpose | Functional interface |
|---|---|---|
filter() | Keep elements matching a condition | Predicate<T> |
map() | Transform each element | Function<T, R> |
flatMap() | Transform and flatten nested streams | Function<T, Stream<R>> |
distinct() | Remove duplicates | uses equals()/hashCode() |
sorted() | Sort elements | Comparator<T> |
peek() | Observe elements (debugging) | Consumer<T> |
limit(n) | Keep first n elements | none |
skip(n) | Skip first n elements | none |
mapToInt() etc. | Convert to a primitive Stream | ToIntFunction<T> |
takeWhile() / dropWhile() | Take or drop while a condition holds (Java 9+) | Predicate<T> |
filter()
Selects elements that satisfy a condition.
List<Integer> result = List.of(1, 2, 3, 4, 5, 6).stream().filter(n -> n > 3).toList();System.out.println(result); // [4, 5, 6]
1Element → Predicate → true → keep2 → false → discardmap()
Transforms each element into something else. The number of elements stays the same.
List<String> result = List.of("HELLO", "EVERYBODY", "JAVA").stream().map(String::toLowerCase).toList();System.out.println(result); // [hello, everybody, java]
1filter → Which elements should remain?2map → What should each element become?flatMap() ⭐
Used for nested structures. It transforms each element into a Stream and then flattens all of them into one Stream.
List<List<String>> data = List.of(List.of("I", "love", "Java"),List.of("Streams", "are", "powerful"));List<String> result = data.stream().flatMap(List::stream).toList();System.out.println(result);// [I, love, Java, Streams, are, powerful]
1map: List<List<String>> → Stream<List<String>> (still nested)2flatMap: List<List<String>> → Stream<String> (flattened)flatMap = map + flatten
distinct()
Removes duplicates using equals() and hashCode().
List<Integer> result = List.of(1, 2, 2, 3, 3, 4, 4, 5).stream().distinct().toList();System.out.println(result); // [1, 2, 3, 4, 5]
sorted()
List<Integer> numbers = List.of(5, 1, 9, 2, 4);// Natural ordernumbers.stream().sorted().toList(); // [1, 2, 4, 5, 9]// Descendingnumbers.stream().sorted(Comparator.reverseOrder()).toList(); // [9, 5, 4, 2, 1]// Custom: sort strings by lengthList.of("banana", "kiwi", "apple").stream().sorted(Comparator.comparing(String::length)).toList(); // [kiwi, apple, banana]
Prefer
Comparator.reverseOrder()over(a, b) -> b - a. The subtraction trick can overflow for large or negative values.
peek()
Performs an action on each element as it passes through. Mainly used for debugging.
List<Integer> result = List.of(1, 2, 3, 4, 5).stream().filter(n -> n > 2).peek(n -> System.out.println("Passed filter: " + n)).toList();
peek()is lazy. Without a terminal operation, nothing is printed.
limit() and skip()
List<Integer> data = List.of(2, 1, 3, 4, 6);data.stream().limit(3).toList(); // [2, 1, 3] (first 3)data.stream().skip(3).toList(); // [4, 6] (skip first 3)// Pagination: page 2, page size 2data.stream().skip(2).limit(2).toList(); // [3, 4]
Primitive Streams ⭐
Streams of Integer use boxing (int ↔ Integer), which costs memory and time. Java provides specialized Streams for primitives.
| Stream | Primitive | Convert using |
|---|---|---|
IntStream | int | mapToInt() |
LongStream | long | mapToLong() |
DoubleStream | double | mapToDouble() |
They also provide handy methods like sum(), average(), min(), max(), and summaryStatistics().
List<String> numbers = List.of("2", "1", "4", "7");int[] arr = numbers.stream().mapToInt(Integer::parseInt).toArray();int sum = IntStream.of(arr).sum(); // 14double avg = IntStream.of(arr).average().orElse(0); // 3.5IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1 2 3 4 5// Back to objectsStream<Integer> boxed = IntStream.of(arr).boxed();
Lazy Evaluation ⭐⭐⭐
Intermediate operations do not run until a terminal operation is called.
List<Integer> numbers = List.of(2, 1, 4, 7, 10);// No terminal operation: NOTHING is printednumbers.stream().filter(n -> n >= 3).peek(System.out::println);// With a terminal operation: pipeline runslong count = numbers.stream().filter(n -> n >= 3).peek(System.out::println).count();// Output:// 4// 7// 10
Intermediate operations are lazy and are triggered by the terminal operation.
How Elements Flow Through the Pipeline
A common misconception is that each operation processes all elements before the next one starts. In reality, elements move through the pipeline one by one.
Stream.of(2, 1, 4, 7).filter(n -> {System.out.println("filter " + n);return n >= 3;}).map(n -> {System.out.println("map " + n);return n * 10;}).forEach(n -> System.out.println("result " + n));// Output:// filter 2// filter 1// filter 4// map 4// result 40// filter 7// map 7// result 70
12 → filter ✗ (stops here)21 → filter ✗ (stops here)34 → filter ✓ → map → forEach47 → filter ✓ → map → forEachThis is called vertical processing, and it is why a Stream does not need to create an intermediate collection after each step.
Stateful Operations
Some operations need to see all (or many) elements before they can produce output. sorted() is the classic example: it must collect everything before emitting the first element. It acts as a barrier in the pipeline, so elements before it are all processed before any element after it.
Short-Circuiting
Some operations can stop early once the answer is known, which is useful for large or infinite Streams.
| Type | Operations |
|---|---|
| Intermediate short-circuit | limit(), takeWhile() |
| Terminal short-circuit | anyMatch(), allMatch(), noneMatch(), findFirst(), findAny() |
1Stream: 1, 2, 4, 7, 10, ... Condition: n > 32 3anyMatch(n -> n > 3): checks 1 ✗, 2 ✗, 4 ✓ → stops, returns trueTerminal Operations ⭐
Terminal operations trigger execution, produce the result, and close the Stream.
| Operation | Purpose | Returns |
|---|---|---|
forEach() | Perform an action on each element | void |
collect() | Gather elements into a collection | Collection / result |
toList() | Gather into an unmodifiable list (Java 16+) | List<T> |
toArray() | Convert to an array | Array |
reduce() | Combine elements into one result | Optional / value |
count() | Count elements | long |
min() / max() | Smallest / largest element | Optional<T> |
findFirst() | First element | Optional<T> |
findAny() | Any element | Optional<T> |
anyMatch() | At least one matches | boolean |
allMatch() | All match | boolean |
noneMatch() | None match | boolean |
forEach(), toArray(), count()
List<Integer> numbers = List.of(2, 1, 4, 7, 10);// forEachnumbers.stream().filter(n -> n >= 3).forEach(System.out::println); // 4 7 10// toArrayObject[] a = numbers.stream().toArray();Integer[] b = numbers.stream().toArray(Integer[]::new); // typed array// countlong count = numbers.stream().filter(n -> n >= 3).count(); // 3
reduce() ⭐⭐⭐
Combines all elements into a single result.
12, 1, 4, 7, 1022 + 1 = 333 + 4 = 747 + 7 = 14514 + 10 = 24List<Integer> numbers = List.of(2, 1, 4, 7, 10);// 1. Without identity: returns Optional (stream might be empty)Optional<Integer> sum1 = numbers.stream().reduce((a, b) -> a + b);System.out.println(sum1.get()); // 24// 2. With identity: returns a plain value (identity is the result for an empty stream)int sum2 = numbers.stream().reduce(0, (a, b) -> a + b);System.out.println(sum2); // 24// Method referenceint sum3 = numbers.stream().reduce(0, Integer::sum);// Multiplicationint product = List.of(2, 1, 4).stream().reduce(1, (a, b) -> a * b); // 8
The operation passed to
reduce()must be associative (grouping must not change the result), so it works correctly with parallel Streams.
collect() and Collectors ⭐
collect() gathers Stream elements into a collection, string, or map using a Collector.
import java.util.stream.Collectors;List<String> names = List.of("Ravi", "Anita", "Kiran", "Amit", "Ravi");// To List / SetList<String> list = names.stream().collect(Collectors.toList());Set<String> set = names.stream().collect(Collectors.toSet());// Joining into a StringString joined = names.stream().collect(Collectors.joining(", ", "[", "]"));// [Ravi, Anita, Kiran, Amit, Ravi]// To Map (keys must be unique, else IllegalStateException)Map<String, Integer> lengths = names.stream().distinct().collect(Collectors.toMap(n -> n, String::length));// Group by first letterMap<Character, List<String>> byLetter = names.stream().collect(Collectors.groupingBy(n -> n.charAt(0)));// {A=[Anita, Amit], K=[Kiran], R=[Ravi, Ravi]}// Count per groupMap<String, Long> freq = names.stream().collect(Collectors.groupingBy(n -> n, Collectors.counting()));// {Ravi=2, Anita=1, Kiran=1, Amit=1}// Partition into two groups (true / false)Map<Boolean, List<String>> parts = names.stream().collect(Collectors.partitioningBy(n -> n.length() > 4));
| Collector | Result |
|---|---|
toList(), toSet() | List or Set |
toMap(k, v) | Map |
joining() | Single String |
groupingBy() | Map of groups |
partitioningBy() | Map with true / false keys |
counting() | Count (Long) |
summingInt(), averagingInt() | Sum / average |
Stream.toList() (Java 16+) returns an unmodifiable list, while Collectors.toList() makes no guarantee about the list type.
min() and max()
Optional<Integer> min = List.of(4, 7, 10).stream().min(Integer::compareTo);Optional<Integer> max = List.of(4, 7, 10).stream().max(Integer::compareTo);System.out.println(min.get()); // 4System.out.println(max.get()); // 10
anyMatch(), allMatch(), noneMatch()
List.of(1, 2, 4, 7).stream().anyMatch(n -> n > 3); // true (at least one)List.of(2, 4, 6, 8).stream().allMatch(n -> n % 2 == 0); // true (all)List.of(1, 3, 5).stream().noneMatch(n -> n % 2 == 0); // true (none)
On an empty Stream: anyMatch is false, while allMatch and noneMatch are true.
findFirst() and findAny()
Optional<Integer> first = List.of(4, 7, 10).stream().findFirst(); // 4Optional<Integer> any = List.of(4, 7, 10).stream().findAny(); // any element
findFirst() always returns the first element. findAny() may return any element, which gives more freedom in parallel Streams.
Optional in Stream Results
min(), max(), findFirst(), findAny(), and the single-argument reduce() return an Optional, because the Stream might be empty.
Optional<Integer> result = numbers.stream().findFirst();result.isPresent(); // true / falseresult.get(); // value (throws if empty)result.orElse(0); // value or defaultresult.ifPresent(System.out::println);
A Stream Cannot Be Reused ⭐⭐⭐
After a terminal operation the Stream is consumed.
Stream<Integer> stream = List.of(1, 2, 3, 4, 5).stream();stream.count(); // terminal operation: stream is now closedstream.filter(n -> n > 2); // ❌ IllegalStateException
1IllegalStateException: stream has already been operated upon or closedFix: create a new Stream from the source each time.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);numbers.stream().count();numbers.stream().filter(n -> n > 2).toList(); // ✅ new Stream
Sequential vs Parallel Streams
| Feature | stream() | parallelStream() |
|---|---|---|
| Processing | One element at a time, one thread | Split into chunks, many threads |
| Order | Maintained | Processing order not guaranteed (forEachOrdered() keeps it) |
| Best for | Most everyday code | Large data, CPU-heavy work |
List<Integer> numbers = List.of(11, 12, 13, 14, 15);numbers.parallelStream().map(n -> n * n).forEach(System.out::println); // order may vary// Or convert an existing streamnumbers.stream().parallel();
1Collection2 ↓3Split into smaller tasks (Spliterator)4 ↓5Task 1 Task 2 Task 3 Task 4 ← run on multiple CPU cores6 ↓7Combine results (join)Behind the Scenes
| Concept | Role |
|---|---|
| Fork/Join framework | Parallel Streams run on the common ForkJoinPool: a big task is forked into smaller ones and the results are joined |
| Spliterator | Traverses the source and splits it into chunks using trySplit() |
When NOT to Use Parallel Streams
| Situation | Reason |
|---|---|
| Small collections / cheap operations | Splitting and merging cost more than the work |
| Operations with shared mutable state | Race conditions |
| Blocking I/O or database calls inside the pipeline | Blocks the shared common pool |
| Order-dependent logic | Order is not preserved |
Poorly splittable sources (e.g. LinkedList) | Splitting is inefficient |
Parallel does not automatically mean faster. Measure first.
Complete Pipeline Example ⭐⭐⭐
List<Integer> numbers = List.of(2, 1, 4, 7, 10);List<Integer> result = numbers.stream().filter(n -> n >= 3).map(n -> -n).sorted().toList();System.out.println(result); // [-10, -7, -4]
12, 1, 4, 7, 102 ↓ filter(n >= 3)34, 7, 104 ↓ map(n -> -n)5-4, -7, -106 ↓ sorted()7-10, -7, -48 ↓ toList()9[-10, -7, -4]Real-World Example: Employees
record Employee(String name, String dept, double salary) { }List<Employee> employees = List.of(new Employee("Ravi", "IT", 80000),new Employee("Anita", "HR", 50000),new Employee("Kiran", "IT", 95000),new Employee("Meena", "HR", 60000));// 1. Names of IT employees earning more than 85000List<String> names = employees.stream().filter(e -> e.dept().equals("IT")).filter(e -> e.salary() > 85000).map(Employee::name).toList(); // [Kiran]// 2. Total salarydouble total = employees.stream().mapToDouble(Employee::salary).sum(); // 285000.0// 3. Highest paid employeeOptional<Employee> top = employees.stream().max(Comparator.comparingDouble(Employee::salary));// 4. Average salary per departmentMap<String, Double> avgByDept = employees.stream().collect(Collectors.groupingBy(Employee::dept,Collectors.averagingDouble(Employee::salary)));// {IT=87500.0, HR=55000.0}
Common Mistakes ⚠️
| Mistake | Problem / Fix |
|---|---|
| Forgetting the terminal operation | Nothing runs. Add toList(), count(), forEach(), etc. |
| Reusing a Stream | IllegalStateException. Create a new Stream |
| Modifying the source collection inside the pipeline | ConcurrentModificationException. Collect into a new list |
Relying on side effects in map() / filter() | Keep lambdas pure (no external state changes) |
Collectors.toMap() with duplicate keys | IllegalStateException. Pass a merge function as the third argument |
Calling get() on an empty Optional | NoSuchElementException. Use orElse() or ifPresent() |
Using parallelStream() everywhere | Often slower. Use only when measured |
Using peek() for real logic | It is meant for debugging. Use forEach() or map() |
Note: since Java 9,
count()may skip the pipeline (and anypeek()) if it can compute the size directly from the source, for examplelist.stream().peek(...).count()with nofilter().
Functional Interfaces Used by Streams ⭐
| Operation | Functional interface | Signature |
|---|---|---|
filter() | Predicate<T> | T → boolean |
map() | Function<T, R> | T → R |
flatMap() | Function<T, Stream<R>> | T → Stream<R> |
peek() | Consumer<T> | T → void |
forEach() | Consumer<T> | T → void |
reduce() | BinaryOperator<T> | (T, T) → T |
sorted() | Comparator<T> | (T, T) → int |
generate() | Supplier<T> | () → T |
Stream Cheat Sheet
1STREAM2│3├── Create4│ ├── collection.stream()5│ ├── Arrays.stream()6│ ├── Stream.of()7│ ├── Stream.builder()8│ ├── Stream.iterate()9│ └── Stream.generate()10│11├── Intermediate (lazy, return Stream)12│ ├── filter() → select13│ ├── map() → transform14│ ├── flatMap() → flatten15│ ├── distinct() → remove duplicates16│ ├── sorted() → sort17│ ├── peek() → observe18│ ├── limit() → first N19│ ├── skip() → skip N20│ └── mapToInt() / mapToLong() / mapToDouble()21│22└── Terminal (triggers execution, closes Stream)23 ├── forEach() ├── min() / max()24 ├── collect() ├── findFirst() / findAny()25 ├── toList() ├── anyMatch() / allMatch() / noneMatch()26 ├── reduce() └── toArray()27 └── count()Interview Questions ⭐
Q1. What is a Stream? A pipeline for processing a sequence of elements from a source using operations like filter, map, and reduce. It does not store data.
Q2. Is a Stream a data structure? No. It does not store elements. It carries them from a source through operations.
Q3. What are the three parts of a Stream pipeline? Source, zero or more intermediate operations, and exactly one terminal operation.
Q4. Are intermediate operations eager or lazy? Lazy. They run only when a terminal operation is invoked.
Q5. Can a Stream have multiple terminal operations? No. The Stream is consumed after the first one.
Q6. Can a Stream be reused?
No. Reuse throws IllegalStateException.
Q7. Does a Stream modify the original collection? No. It produces a new result and leaves the source unchanged.
Q8. Difference between map() and flatMap()?
map() is one-to-one. flatMap() maps each element to a Stream and flattens them into one.
Q9. Difference between filter() and map()?
filter() selects elements. map() transforms them.
Q10. Why does peek() sometimes print nothing?
It is lazy. Without a terminal operation the pipeline never runs.
Q11. Difference between findFirst() and findAny()?
findFirst() returns the first element. findAny() may return any element, which matters in parallel Streams.
Q12. Difference between stream() and parallelStream()?
stream() is sequential. parallelStream() splits the work across multiple threads using the Fork/Join pool.
Q13. What is a Spliterator?
An object that traverses a source and can split it (trySplit()) into chunks for parallel processing.
Q14. Difference between Collection and Stream?
A Collection stores data and is reusable. A Stream processes data lazily and is single-use.
Q15. What is a short-circuiting operation?
One that can stop early without processing all elements, such as limit(), anyMatch(), and findFirst().
Q16. Difference between collect() and reduce()?
reduce() combines elements into one immutable-style value. collect() is a mutable reduction that accumulates into containers like a List or Map.
Interview Definition
A Java Stream is a lazily evaluated, single-use pipeline of operations (intermediate and terminal) that processes elements from a data source in a declarative, functional style without modifying the source.
Remember
Source → Intermediate (lazy, returns Stream) → Terminal (triggers, closes Stream). A Stream cannot be reused.
parallelStream()is not always faster.