Java Streams API — A Modern Approach to Data Processing
The Streams API, introduced in Java 8, offers a functional-style way to process sequences of elements (such as collections, arrays, or I/O channels) using a pipeline of operations like map
, filter
, and reduce
.
- Streams don’t store data — they operate on data.
- They are non-destructive, lazy, and fluent.
🔹 Key Characteristics of Streams
Feature | Description |
Functional-style | Supports operations like map, filter, and reduce |
Pipeline-based | Chainable intermediate and terminal operations |
Immutable source | Original data (collection/array) is not modified |
Lazy evaluation | Intermediate operations are executed only when a terminal operation runs |
Not a data structure | Acts as a view or abstraction over the source, not a container itself |
🔹 Stream Pipeline Structure
dataSource.stream()
.intermediateOperation1()
.intermediateOperation2()
...
.terminalOperation();
- Source: Collection, array, or generator (e.g., List, Set, Stream.of())
- Intermediate operations: Return a new stream (lazy)
- Terminal operations: Produce a result or side effect (eager)
Source (Stream Creation) :
Stream.of("A", "B", "C"); // From varargs
Arrays.stream(new int[]{1, 2, 3}); // From array
List.of("X", "Y").stream(); // From collection
IntStream.range(1, 5); // 1 to 4
IntStream.rangeClosed(1, 5); // 1 to 5
Stream.of("A", "B", "C"); // From varargs
Arrays.stream(new int[]{1, 2, 3}); // From array
List.of("X", "Y").stream(); // From collection
IntStream.range(1, 5); // 1 to 4
IntStream.rangeClosed(1, 5); // 1 to 5
Intermediate Operations (Return a Stream) :
Method | Purpose |
filter(Predicate) | Filters elements based on condition |
map(Function) | Transforms each element |
flatMap(Function) | Flattens nested structures |
distinct() | Removes duplicates |
sorted() | Natural sort |
sorted(Comparator) | Custom sorting logic |
limit(n) | Truncates to first n elements |
skip(n) | Skips first n elements |
peek(Consumer) | Debug/logging in the pipeline |
Terminal Operations (Trigger Execution) :
Method | Purpose |
forEach(Consumer) | Performs action for each element |
toArray() | Collects elements into array |
reduce(...) | Aggregates (sum, min, etc.) |
collect(...) | Collects into collection/result |
min(Comparator) | Finds minimum element |
max(Comparator) | Finds maximum element |
count() | Counts elements |
anyMatch(Predicate) | Checks if any element matches condition |
allMatch(Predicate) | Checks if all match condition |
noneMatch(Predicate) | Checks if none match condition |
findFirst() | Returns first element |
findAny() | Returns any element (useful with parallel) |
Common Collectors (Used with collect()
) :
Collector | Purpose |
Collectors.toList() | Collects to a List |
Collectors.toSet() | Collects to a Set |
Collectors.toMap(k,v) | Collects to a Map |
Collectors.joining(",") | Concatenates strings |
Collectors.groupingBy() | Groups by field/key |
Collectors.partitioningBy() | Splits into true/false groups |
Collectors.counting() | Counts elements |
Collectors.summarizingInt() | Stats like min/max/avg/sum/count |
Example :List<Employee> itEmployees = employees.stream()
.filter(e -> "IT".equals(e.getDepartment()))
.collect(Collectors.toList());System.out.println("IT Employees: " + itEmployees);
.filter(e -> "IT".equals(e.getDepartment()))
.collect(Collectors.toList());
🔹 Parallel Streams for Performance
List<Integer> bigList = IntStream.range(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
long evenCount = bigList.parallelStream()
.filter(i -> i % 2 == 0)
.count();
System.out.println(evenCount); // Output: 499999
🔹 Lambda Expressions with Streams
Lambda expressions offer concise functional logic without boilerplate:
Syntax: (parameters) -> expression
Example: Filter employees with salary > 50,000 and print their uppercase names
Use when:
- You need conditions, transformations, or side-effects
- The logic can’t be replaced by a single existing method
::
operator, often replacing simple lambdas.Syntax: ClassName::methodName
or object::methodName
Example: Get employee names and print them
Use when:
- You're simply calling an existing method
- It enhances readability and avoids unnecessary lambda syntax
Prefer Lambda When... | Prefer Method Reference When... |
You need logic like e -> e.getX() + 10 | You just call e.getX() or System.out::println |
You combine or transform values | You're passing method as-is |
Feature | Stream | Collection |
Iteration | Internal (managed by Stream) | External (for-loop, iterator) |
Reusability | No (one-time use) | Yes |
Evaluation | Lazy | Eager |
Parallelism | Supported (parallelStream()) | Not supported directly |
Data Modification | Not allowed | Allowed |
🔹 When to use Streams
- You want to transform or filter data cleanly
- You need to aggregate data (sum, groupBy, etc.)
- You prefer declarative, readable code
Avoid Streams when:
- You need index-based access
You require side-effects or complex mutable state