Java Stream API –
1. Introduction to Java Stream API
The Stream API was introduced in Java 8 to process collections of data in a functional programming style.
It allows performing bulk operations (like filtering, mapping, reducing, etc.) on data with ease.
Key Points:
- A Stream represents a sequence of elements.
- It does not store data; it provides a pipeline to process data.
- Stream operations are functional (use lambda expressions, functional interfaces).
- Stream is different from java.io.Stream (input/output streams).
2. Why Use Streams?
Before Java 8, to filter or transform data, developers had to use loops and iterators, making code verbose and less readable.
With Stream API:
- Less boilerplate code
- Readability (code looks closer to SQL queries)
- Parallel processing (multi-core CPUs utilized)
- Declarative style (focus on what to do, not how to do)
3. Stream API Workflow
A Stream operation generally has 3 stages:
- Source – Where data comes from
Examples: Collection, Arrays, I/O channels, or Stream.of() - Intermediate Operations – Transform the stream (return a new stream)
Examples: filter(), map(), sorted() - Terminal Operations – Produce a result (end the stream)
Examples: collect(), forEach(), reduce()
You’re right to check that — in my earlier draft I explained what Streams are, their operations, and examples, but I did not explicitly include a “How to Create a Java Stream?” section.
That’s an important part, because before performing operations we must know how to instantiate a Stream. Let me add that section for you in a detailed theory + practical style.
4. How to Create a Java Stream?
Streams in Java can be created in multiple ways depending on the data source. Here are the main approaches:
1. From Collections
Every collection class (List, Set, etc.) in Java has a built-in method stream() that creates a sequential stream, and parallelStream() that creates a parallel stream.
Example:
import java.util.*;
import java.util.stream.*;
public class CollectionStreamExample {
public static void main(String[] args) {
List names = Arrays.asList("Rahul", "Amit", "Neha", "Priya");
// Sequential stream
Stream sequentialStream = names.stream();
sequentialStream.forEach(System.out::println);
// Parallel stream
Stream parallelStream = names.parallelStream();
parallelStream.forEach(System.out::println);
}
}
2. Using Arrays
The Arrays.stream() method or Stream.of() can be used to create streams from arrays.
Example:
import java.util.stream.*;
public class ArrayStreamExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
// Stream from array
IntStream intStream = Arrays.stream(numbers);
intStream.forEach(System.out::println);
// Another way
Stream numberStream = Stream.of(10, 20, 30, 40);
numberStream.forEach(System.out::println);
}
}
3. Using Stream.of()
Directly create a stream from given values.
Example:
import java.util.stream.*;
public class StreamOfExample {
public static void main(String[] args) {
Stream stream = Stream.of("Java", "Python", "C++", "Go");
stream.forEach(System.out::println);
}
}
4. Using Stream.generate()
Used to create infinite streams by supplying a Supplier function.
Example:
import java.util.stream.*;
public class StreamGenerateExample {
public static void main(String[] args) {
// Generates random numbers
Stream randomNumbers = Stream.generate(Math::random);
// Limiting to 5 elements
randomNumbers.limit(5).forEach(System.out::println);
}
}
5. Using Stream.iterate()
Also creates infinite streams but by applying a function iteratively.
Example:
import java.util.stream.*;
public class StreamIterateExample {
public static void main(String[] args) {
// Start with 1, keep adding 2
Stream oddNumbers = Stream.iterate(1, n -> n + 2);
// Limiting to 5 elements
oddNumbers.limit(5).forEach(System.out::println);
}
}
6. Using Files.lines() (Stream from a File)
The Files.lines() method can create a stream of lines from a text file.
Example:
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;
public class FileStreamExample {
public static void main(String[] args) throws IOException {
Stream lines = Files.lines(Paths.get("example.txt"));
lines.forEach(System.out::println);
lines.close();
}
}
5. Intermediate Operations (Transformations)
Intermediate operations are operations performed on a Stream that transform it into another stream.
They are lazy, meaning they don’t execute immediately. They only get executed when a terminal operation (like forEach, collect, reduce) is called.
Multiple intermediate operations can be chained together, forming a pipeline.
Intermediate operations return a new stream, allowing method chaining. They are lazy, meaning they don’t run immediately — execution happens only when a terminal operation (like forEach(), collect()) is called.
(a) filter()
- Used to select elements that match a given condition.
- Returns a stream containing only those elements that satisfy the predicate.
- Often used for conditional processing.
Key Point: Doesn’t modify elements, only reduces the stream based on condition.
List nums = Arrays.asList(10, 20, 30, 40);
nums.stream()
.filter(n -> n > 20)
.forEach(System.out::println); // Output: 30, 40
(b) map()
- Used to transform each element into another form.
- Accepts a function and applies it to every element.
- Most commonly used for data conversion (e.g., lowercase → uppercase, number → square).
Key Point: The number of elements usually remains the same, but their type or value changes.
List names = Arrays.asList("john", "doe");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // Output: JOHN, DOE
(c) flatMap()
- Used when elements themselves are collections (or streams).
- Converts each sub-collection into a stream, then flattens them into a single stream.
- Helps avoid nested collections.
Key Point: map() → returns stream of collections, flatMap() → merges them into one stream.
List> list = Arrays.asList(
Arrays.asList("A", "B"),
Arrays.asList("C", "D")
);
list.stream()
.flatMap(Collection::stream)
.forEach(System.out::println); // Output: A, B, C, D
(d) distinct()
- Removes duplicate elements.
- Uses equals() method for comparison.
- Ensures uniqueness in the stream.
Stream.of(1, 2, 2, 3, 3, 4)
.distinct()
.forEach(System.out::println); // Output: 1, 2, 3, 4
(e) sorted()
- Sorts stream elements.
- By default, sorts in natural order.
- Can also accept a custom Comparator.
Stream.of(5, 1, 3, 2)
.sorted()
.forEach(System.out::println); // Output: 1, 2, 3, 5
Custom Comparator:
Stream.of("apple", "banana", "cherry")
.sorted(Comparator.reverseOrder())
.forEach(System.out::println); // Output: cherry, banana, apple
(f) limit() & skip()
- limit(n): restricts stream to first n elements.
- skip(n): ignores the first n elements and processes the rest.
- Useful in pagination or sampling.
Stream.of(10, 20, 30, 40, 50)
.limit(3)
.forEach(System.out::println); // Output: 10, 20, 30
Stream.of(10, 20, 30, 40, 50)
.skip(2)
.forEach(System.out::println); // Output: 30, 40, 50
6. Terminal Operations (Results)
Terminal operations are the final operations on a stream. They produce a result (non-stream value like int, List, Optional, etc.) or a side-effect (like printing).
After a terminal operation, the stream is consumed and cannot be reused.
(a) forEach()
- Performs an action for each element of the stream.
- Usually used for printing or applying side effects.
Stream.of("A", "B", "C")
.forEach(System.out::println);
// Output: A B C
(b) toArray()
- Collects elements into an array.
String[] arr = Stream.of("X", "Y", "Z")
.toArray(String[]::new);
System.out.println(Arrays.toString(arr));
// Output: [X, Y, Z]
(c) reduce()
- Reduces elements to a single value (aggregation).
int sum = Stream.of(1, 2, 3, 4)
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 10
(d) collect()
- Collects results into a collection or map.
- Most powerful terminal operation.
List list = Stream.of("apple", "banana", "apple")
.collect(Collectors.toList());
System.out.println(list);
// Output: [apple, banana, apple]
Set set = Stream.of("apple", "banana", "apple")
.collect(Collectors.toSet());
System.out.println(set);
// Output: [apple, banana]
(e) min() and max()
- Finds the minimum or maximum element based on a comparator.
int min = Stream.of(5, 9, 1, 7)
.min(Integer::compare)
.get();
System.out.println(min); // 1
int max = Stream.of(5, 9, 1, 7)
.max(Integer::compare)
.get();
System.out.println(max); // 9
(f) count()
- Returns the number of elements in the stream.
long count = Stream.of(10, 20, 30, 40)
.count();
System.out.println(count); // 4
(g) anyMatch(), allMatch(), noneMatch()
- Used for checking conditions.
boolean any = Stream.of(1, 2, 3, 4)
.anyMatch(n -> n > 3);
System.out.println(any); // true
boolean all = Stream.of(1, 2, 3, 4)
.allMatch(n -> n > 0);
System.out.println(all); // true
boolean none = Stream.of(1, 2, 3, 4)
.noneMatch(n -> n < 0);
System.out.println(none); // true
(h) findFirst() and findAny()
- Find first or any element in the stream (returns Optional).
Optional first = Stream.of(10, 20, 30)
.findFirst();
System.out.println(first.get()); // 10
Optional any = Stream.of(10, 20, 30)
.findAny();
System.out.println(any.get()); // could be 10, 20, or 30
7. Collectors Utility Class
The Collectors class (in java.util.stream.Collectors) is a utility class that provides a variety of static methods to perform reduction operations on stream elements and return them in a desired collection or result type. It is part of the java.util.stream package and works hand-in-hand with the collect() terminal operation.
Collectors are essential when you want to transform a stream into a data structure (like a List, Set, or Map), aggregate values (like counting or averaging), or perform grouping and partitioning.
7.1. Key Features of Collectors
- Immutable Results – The result of a collector operation is often an immutable collection or final value.
- Flexible Data Reduction – Provides wide support for reduction into collections, strings, maps, numeric aggregates, etc.
- Custom Collectors – Developers can build custom collectors using Collector.of(...).
- Parallel Friendly – Collectors are designed to work in both sequential and parallel stream pipelines.
7.2. Commonly Used Collectors Methods
a) toList(), toSet(), toMap()
- Collect elements into a List, Set, or Map.
- toList() → Collects into a List (e.g., ArrayList).
- toSet() → Collects into a Set (eliminates duplicates).
- toMap(keyMapper, valueMapper) → Collects into a Map with custom key/value extraction functions.
b) joining()
- Concatenates the stream of CharSequence elements into a single String.
- Overloaded methods allow specifying a delimiter, prefix, and suffix.
Example:
List names = Arrays.asList("John", "Jane", "Jack");
String result = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(result);
// Output: [John, Jane, Jack]
c) groupingBy()
- Groups stream elements based on a classification function and returns a Map> by default.
- Overloads allow grouping into different collection types and applying downstream collectors (e.g., counting, summing).
Example:
Map> grouped =
names.stream().collect(Collectors.groupingBy(name -> name.charAt(0)));
d) partitioningBy()
- Partitions stream elements into two groups (true/false) based on a given predicate.
- Returns a Map>.
- Useful for binary classification.
Example:
Map> partitioned =
names.stream().collect(Collectors.partitioningBy(name -> name.length() > 3));
7.3. Advanced Collectors
- counting() → Counts elements.
- summingInt(), summingDouble(), summingLong() → Summation of numerical values.
- averagingInt(), averagingDouble(), averagingLong() → Computes average.
- maxBy(comparator), minBy(comparator) → Finds max/min element.
- collectingAndThen(collector, finisher) → Wraps another collector and applies a finishing function.
- mapping(mapper, downstream) → Applies a mapping function before reduction.
7.4. Practical Importance
- Data Transformation → Transform streams into collections/maps for further processing.
- Aggregation → Easily calculate sum, average, count, min, and max.
- Grouping and Partitioning → Simplifies classification and categorization of data.
- Readable and Declarative → Makes code concise compared to imperative looping.
8. Parallel Streams
The Stream API not only provides sequential stream processing but also supports parallel execution to take advantage of multi-core processors. A parallel stream divides the given data source into multiple chunks, processes them in parallel across different threads, and then combines the results.
Key Concepts of Parallel Streams:
Creation of Parallel Stream
- You can obtain a parallel stream in two ways:
- By calling .parallelStream() on a Collection.
- By calling .parallel() on an existing sequential stream.
List numbers = Arrays.asList(1,2,3,4,5,6,7,8); numbers.parallelStream().forEach(System.out::println);
Here, elements may be printed in any order because execution happens concurrently.
- You can obtain a parallel stream in two ways:
- Work Splitting (Fork/Join Framework)
- Internally, Java uses the Fork/Join framework (introduced in Java 7) to split the data source into sub-tasks.
- Each sub-task is processed by a different worker thread in the common ForkJoinPool.
- Once all sub-tasks finish execution, results are merged.
- Performance Considerations
- Large Datasets → Parallel streams perform better when dealing with large collections.
- Small Datasets → Parallel overhead (splitting + thread management) can make it slower than sequential streams.
- CPU-Intensive Operations → Best suited when tasks are computationally heavy.
- I/O-Bound Operations → Not beneficial, as threads may block.
Ordering Issues
- Parallel streams do not guarantee order unless you explicitly use forEachOrdered().
numbers.parallelStream() .forEachOrdered(System.out::println); // Maintains order
- Thread-Safety Concerns
- Since execution happens concurrently, shared mutable state must be avoided.
- For example, updating a global ArrayList inside a parallel stream can cause race conditions.
- Always use collectors or thread-safe data structures (ConcurrentHashMap, CopyOnWriteArrayList) for parallel operations.
When to Use Parallel Streams
✅ Suitable for:- Large data collections
- CPU-bound operations
- Stateless and non-blocking tasks
❌ Avoid in:
- Small collections (overhead > benefit)
- I/O heavy tasks (blocking threads)
- Tasks requiring strict ordering
- Example: Summing with Parallel Streams
List list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
int sum = list.parallelStream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum: " + sum); // Output: 55
9. Practical Use Case – Employee Filtering
class Employee {
String name;
double salary;
Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
public class EmployeeStream {
public static void main(String[] args) {
List employees = Arrays.asList(
new Employee("John", 50000),
new Employee("Jane", 60000),
new Employee("Jack", 40000)
);
// Find employees with salary > 45,000
employees.stream()
.filter(e -> e.salary > 45000)
.map(e -> e.name)
.forEach(System.out::println); // John, Jane
}
}
10. Key Differences – Stream vs Collection
Feature | Collection | Stream |
---|---|---|
Storage | Stores elements | Does not store elements |
Iteration | External (loops, iterators) | Internal (stream pipeline) |
Mutability | Mutable | Immutable |
Evaluation | Eager | Lazy |
Processing | Sequential | Sequential or Parallel |
11. Advantages of Stream API
- Clean & declarative code
- Supports functional programming
- Lazy evaluation (efficient)
- Parallel execution support
- Rich set of built-in operations
Key Takeaways from Java Stream API
- Streams are not Collections – They provide a pipeline for data processing, not a data structure for storage.
- Functional Programming in Java – Streams bring declarative, functional-style operations (map, filter, reduce) to collections.
- Lazy Evaluation – Intermediate operations (like map, filter) are lazy and executed only when a terminal operation is invoked.
- Powerful Intermediate Operations – Enable transformation, filtering, mapping, sorting, distinct values, and more.
- Terminal Operations Produce Results – Such as aggregation (reduce, count), collection (collect), or side-effects (forEach).
- Collectors Utility Class – Provides advanced collection capabilities like groupingBy, partitioningBy, joining, and mapping results into lists, sets, or maps.
- Parallel Streams – Allow multi-threaded execution but should be used cautiously due to non-deterministic ordering and potential overhead.
- Readable & Concise Code – Complex data processing tasks can be written in a clean and maintainable way using streams.
- Performance Considerations – While streams improve readability, they don’t always outperform traditional loops; careful use is required in performance-critical applications.
Modern Java Development – Mastery of the Stream API is essential for writing efficient, elegant, and modern Java code.