Streams API in Java

Learn how to process data collections in a powerful, elegant way

Think of Streams like a conveyor belt in a factory! Items (data) move on the belt, and at each station, workers do something to them - sort them, filter out bad ones, transform them, etc. Streams let you process lists of data in the same way - step by step, clean and efficient!

What is a Stream?

A Stream is NOT a data structure - it's a sequence of elements that you can process. Think of it as a pipeline where data flows through and gets transformed along the way. Streams don't store data; they carry it from a source (like a List) through operations.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.*;
import java.util.stream.*;
public class StreamBasics {
public static void main(String[] args) {
// Traditional way: Process a list
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = new ArrayList<>();
for (Integer num : numbers) {
if (num % 2 == 0) { // Filter even numbers
doubled.add(num * 2); // Double them
}
}
System.out.println("Traditional: " + doubled); // [4, 8]
// Stream way: Much cleaner!
List<Integer> streamDoubled = numbers.stream() // Create stream
.filter(num -> num % 2 == 0) // Filter even numbers
.map(num -> num * 2) // Double them
.collect(Collectors.toList()); // Collect results
System.out.println("Stream: " + streamDoubled); // [4, 8]
// Visual representation:
// numbers → [1, 2, 3, 4, 5]
// ↓ .stream()
// stream → [1, 2, 3, 4, 5] (flowing data)
// ↓ .filter(even)
// stream → [2, 4] (filtered)
// ↓ .map(* 2)
// stream → [4, 8] (transformed)
// ↓ .collect()
// result → [4, 8] (final list)
// Key point: Streams are PIPELINES, not collections!
}
}

Creating Streams

You can create streams from various sources:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.*;
import java.util.stream.*;
public class CreatingStreams {
public static void main(String[] args) {
// 1. From Collections (most common)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> namesStream = names.stream();
namesStream.forEach(System.out::println);
// 2. From Arrays
String[] array = {"Apple", "Banana", "Cherry"};
Stream<String> arrayStream = Arrays.stream(array);
arrayStream.forEach(System.out::println);
// 3. Using Stream.of()
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
numberStream.forEach(System.out::println);
// 4. Empty stream
Stream<String> emptyStream = Stream.empty();
// 5. Infinite streams
// Generate infinite stream of random numbers
Stream<Double> randomStream = Stream.generate(Math::random);
randomStream.limit(5).forEach(System.out::println); // Take only 5
// Infinite stream with iteration
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2);
infiniteStream.limit(10).forEach(System.out::println); // 0, 2, 4, 6...
// 6. From String
IntStream charStream = "hello".chars();
charStream.forEach(ch -> System.out.print((char)ch + " ")); // h e l l o
// 7. Range streams (for numbers)
IntStream range = IntStream.range(1, 6); // 1 to 5 (exclusive 6)
range.forEach(System.out::println); // 1, 2, 3, 4, 5
IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1 to 5 (inclusive)
rangeClosed.forEach(System.out::println); // 1, 2, 3, 4, 5
// 8. From Files
// Stream<String> lines = Files.lines(Paths.get("file.txt"));
// lines.forEach(System.out::println);
// Remember: You can only use a stream ONCE!
// After terminal operation, create a NEW stream if needed.
}
}

Intermediate Operations

These operations transform a stream into another stream. They are lazy - they don't execute until a terminal operation is called!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import java.util.*;
import java.util.stream.*;
public class IntermediateOperations {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 1. filter() - Keep only elements that match condition
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Even: " + evenNumbers); // [2, 4, 6, 8, 10]
// 2. map() - Transform each element
List<Integer> squared = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("Squared: " + squared); // [1, 4, 9, 16, 25...]
// 3. flatMap() - Flatten nested structures
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4),
Arrays.asList(5, 6)
);
List<Integer> flattened = nested.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println("Flattened: " + flattened); // [1, 2, 3, 4, 5, 6]
// 4. distinct() - Remove duplicates
List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
List<Integer> unique = duplicates.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Unique: " + unique); // [1, 2, 3, 4, 5]
// 5. sorted() - Sort elements
List<Integer> unsorted = Arrays.asList(5, 2, 8, 1, 9, 3);
List<Integer> sorted = unsorted.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("Sorted: " + sorted); // [1, 2, 3, 5, 8, 9]
// Sort in reverse
List<Integer> reversed = unsorted.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println("Reversed: " + reversed); // [9, 8, 5, 3, 2, 1]
// 6. limit() - Take first N elements
List<Integer> first5 = numbers.stream()
.limit(5)
.collect(Collectors.toList());
System.out.println("First 5: " + first5); // [1, 2, 3, 4, 5]
// 7. skip() - Skip first N elements
List<Integer> after5 = numbers.stream()
.skip(5)
.collect(Collectors.toList());
System.out.println("After 5: " + after5); // [6, 7, 8, 9, 10]
// 8. peek() - Perform action without modifying stream (for debugging)
List<Integer> peeked = numbers.stream()
.peek(n -> System.out.println("Processing: " + n))
.filter(n -> n > 5)
.peek(n -> System.out.println("After filter: " + n))
.collect(Collectors.toList());
// Chaining multiple operations
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // Keep even
.map(n -> n * 2) // Double them
.sorted() // Sort
.limit(3) // Take first 3
.collect(Collectors.toList());
System.out.println("\nChained result: " + result); // [4, 8, 12]
// Remember: These operations are LAZY!
// They don't execute until a terminal operation is called!
}
}

Terminal Operations

These operations produce a result or side-effect and close the stream. Once you call a terminal operation, the stream can't be used again!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import java.util.*;
import java.util.stream.*;
public class TerminalOperations {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 1. forEach() - Perform action on each element
System.out.print("Numbers: ");
numbers.stream().forEach(n -> System.out.print(n + " "));
System.out.println();
// 2. collect() - Collect stream into collection
List<Integer> list = numbers.stream()
.filter(n -> n > 5)
.collect(Collectors.toList());
System.out.println("List: " + list);
Set<Integer> set = numbers.stream()
.collect(Collectors.toSet());
System.out.println("Set: " + set);
// 3. count() - Count elements
long count = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
System.out.println("Even count: " + count); // 5
// 4. anyMatch() - Check if ANY element matches
boolean hasEven = numbers.stream()
.anyMatch(n -> n % 2 == 0);
System.out.println("Has even? " + hasEven); // true
// 5. allMatch() - Check if ALL elements match
boolean allPositive = numbers.stream()
.allMatch(n -> n > 0);
System.out.println("All positive? " + allPositive); // true
// 6. noneMatch() - Check if NO elements match
boolean noNegative = numbers.stream()
.noneMatch(n -> n < 0);
System.out.println("No negative? " + noNegative); // true
// 7. findFirst() - Get first element
Optional<Integer> first = numbers.stream()
.filter(n -> n > 5)
.findFirst();
System.out.println("First > 5: " + first.orElse(-1)); // 6
// 8. findAny() - Get any element (useful with parallel streams)
Optional<Integer> any = numbers.stream()
.filter(n -> n > 5)
.findAny();
System.out.println("Any > 5: " + any.orElse(-1));
// 9. min() / max() - Find minimum/maximum
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
System.out.println("Min: " + min.orElse(-1)); // 1
System.out.println("Max: " + max.orElse(-1)); // 10
// 10. reduce() - Combine elements into single result
// Sum all numbers
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println("Sum: " + sum.orElse(0)); // 55
// With initial value
Integer product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Product: " + product);
// 11. toArray() - Convert to array
Integer[] array = numbers.stream()
.filter(n -> n % 2 == 0)
.toArray(Integer[]::new);
System.out.println("Array: " + Arrays.toString(array));
// Advanced collectors
// Join strings
List<String> words = Arrays.asList("Hello", "World", "Java", "Streams");
String joined = words.stream()
.collect(Collectors.joining(", "));
System.out.println("\nJoined: " + joined); // Hello, World, Java, Streams
// Group by length
Map<Integer, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("Grouped by length: " + grouped);
// Partition by condition
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println("Even: " + partitioned.get(true));
System.out.println("Odd: " + partitioned.get(false));
// Statistics
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("\nStats: " + stats);
System.out.println("Average: " + stats.getAverage());
}
}

Common Stream Patterns

Let's see practical examples of how Streams are used:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import java.util.*;
import java.util.stream.*;
public class StreamPatterns {
public static void main(String[] args) {
// Sample data
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 30, 60000),
new Person("Charlie", 35, 70000),
new Person("David", 28, 55000),
new Person("Eve", 32, 65000)
);
// Pattern 1: Filter and collect
List<Person> youngPeople = people.stream()
.filter(p -> p.age < 30)
.collect(Collectors.toList());
System.out.println("Young people: " + youngPeople.size());
// Pattern 2: Map to extract property
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
System.out.println("Names: " + names);
// Pattern 3: Calculate sum/average
double totalSalary = people.stream()
.mapToDouble(Person::getSalary)
.sum();
System.out.println("Total salary: $" + totalSalary);
double avgSalary = people.stream()
.mapToDouble(Person::getSalary)
.average()
.orElse(0);
System.out.println("Average salary: $" + avgSalary);
// Pattern 4: Sort by property
List<Person> sortedByAge = people.stream()
.sorted(Comparator.comparing(Person::getAge))
.collect(Collectors.toList());
System.out.println("\nSorted by age:");
sortedByAge.forEach(p -> System.out.println(p.name + ": " + p.age));
// Pattern 5: Find maximum/minimum
Optional<Person> youngest = people.stream()
.min(Comparator.comparing(Person::getAge));
youngest.ifPresent(p -> System.out.println("\nYoungest: " + p.name));
Optional<Person> highestPaid = people.stream()
.max(Comparator.comparing(Person::getSalary));
highestPaid.ifPresent(p -> System.out.println("Highest paid: " + p.name));
// Pattern 6: Group by property
Map<Integer, List<Person>> byAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("\nGrouped by age: " + byAge.keySet());
// Pattern 7: Partition by condition
Map<Boolean, List<Person>> partitioned = people.stream()
.collect(Collectors.partitioningBy(p -> p.salary > 60000));
System.out.println("High earners: " + partitioned.get(true).size());
System.out.println("Others: " + partitioned.get(false).size());
// Pattern 8: Convert to Map
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAge
));
System.out.println("\nName to age map: " + nameToAge);
// Pattern 9: Check conditions
boolean anyYoung = people.stream()
.anyMatch(p -> p.age < 25);
System.out.println("\nAny under 25? " + anyYoung);
boolean allAdults = people.stream()
.allMatch(p -> p.age >= 18);
System.out.println("All adults? " + allAdults);
// Pattern 10: Complex filtering and transformation
List<String> richYoungNames = people.stream()
.filter(p -> p.age < 35)
.filter(p -> p.salary > 55000)
.map(Person::getName)
.sorted()
.collect(Collectors.toList());
System.out.println("\nRich young people: " + richYoungNames);
// Pattern 11: FlatMap for nested collections
List<List<Integer>> nestedNumbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
List<Integer> allNumbers = nestedNumbers.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println("\nFlattened numbers: " + allNumbers);
// Pattern 12: Distinct values
List<Integer> ages = people.stream()
.map(Person::getAge)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println("Unique ages: " + ages);
}
}
class Person {
String name;
int age;
double salary;
Person(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
String getName() { return name; }
int getAge() { return age; }
double getSalary() { return salary; }
}

Parallel Streams

Parallel streams process data in parallel using multiple threads for better performance on large datasets:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.util.*;
import java.util.stream.*;
public class ParallelStreams {
public static void main(String[] args) {
// Create large dataset
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
// Sequential stream (single thread)
long startSeq = System.currentTimeMillis();
long sumSeq = numbers.stream()
.mapToLong(Integer::longValue)
.sum();
long endSeq = System.currentTimeMillis();
System.out.println("Sequential sum: " + sumSeq);
System.out.println("Sequential time: " + (endSeq - startSeq) + "ms");
// Parallel stream (multiple threads)
long startPar = System.currentTimeMillis();
long sumPar = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
long endPar = System.currentTimeMillis();
System.out.println("\nParallel sum: " + sumPar);
System.out.println("Parallel time: " + (endPar - startPar) + "ms");
// Converting to parallel stream
long count1 = numbers.stream()
.parallel() // Convert to parallel
.filter(n -> n % 2 == 0)
.count();
System.out.println("\nEven count (parallel): " + count1);
// When to use parallel streams
// ✓ GOOD: Large datasets (10,000+ elements)
// ✓ GOOD: CPU-intensive operations
// ✓ GOOD: Independent operations (no shared state)
// ✗ BAD: Small datasets (overhead > benefit)
// ✗ BAD: I/O operations (disk, network)
// ✗ BAD: Operations that modify shared state
// Example: Parallel filtering and mapping
List<Integer> result = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.limit(100)
.collect(Collectors.toList());
System.out.println("\nParallel result size: " + result.size());
// WARNING: Order may not be preserved in parallel streams!
// Use forEachOrdered() to maintain order
System.out.println("\nFirst 10 (unordered):");
numbers.parallelStream()
.limit(10)
.forEach(n -> System.out.print(n + " ")); // Order not guaranteed!
System.out.println("\n\nFirst 10 (ordered):");
numbers.parallelStream()
.limit(10)
.forEachOrdered(n -> System.out.print(n + " ")); // Order maintained
// Check parallelism level
System.out.println("\n\nAvailable processors: " +
Runtime.getRuntime().availableProcessors());
// Remember: Measure performance before using parallel streams!
// They're not always faster - test with your actual data!
}
}

Key Concepts

Lazy Evaluation

Intermediate operations don't execute until a terminal operation is called. This makes streams efficient!

No Storage

Streams don't store data - they carry data from a source through a pipeline of operations.

Functional Style

Streams use lambda expressions and functional programming style for clean, readable code.

One-Time Use

Once a stream is consumed (terminal operation called), it cannot be reused. Create a new stream if needed!

Best Practices

  • Use streams for complex data processing, not simple iterations
  • Don't modify the source collection while streaming
  • Use parallel streams only for large datasets (overhead for small ones)
  • Avoid stateful lambda expressions in stream operations
  • Prefer method references over lambda expressions when possible
  • Use appropriate collectors for better performance

Common Mistakes

Reusing a stream after terminal operation

Why it's wrong: Streams can only be used once. After a terminal operation, create a new stream!

Using streams for simple iterations

Why it's wrong: For simple loops, traditional for-each is often clearer and faster.

Modifying source collection during streaming

Why it's wrong: This causes ConcurrentModificationException or unpredictable behavior!

Overusing parallel streams

Why it's wrong: Parallel streams have overhead. Use only for large datasets or heavy computations.

Interview Tips

  • 💡Explain that Streams are a Java 8 feature for functional-style data processing
  • 💡Know the difference between intermediate and terminal operations
  • 💡Understand lazy evaluation and how it improves performance
  • 💡Be able to write common stream operations: filter, map, reduce, collect
  • 💡Explain when to use parallel streams vs sequential streams
  • 💡Know the common collectors: toList, toSet, toMap, groupingBy, joining