7. Java Streams Overview
Java Streams are a powerful feature introduced in Java 8 that simplify processing sequences of elements, such as collections, arrays, or I/O channels. Streams allow you to perform operations like filtering, mapping, and reducing in a more declarative and functional programming style. Unlike collections, streams do not store elements; instead, they convey them from a source and process them through a pipeline of intermediate and terminal operations.
Key Characteristics of Streams
- Lazy Evaluation: Intermediate operations are not executed until a terminal operation is invoked. This improves efficiency.
- Functional Programming: Streams promote functional programming by using lambda expressions and method references.
- Stateless: Most stream operations do not modify the underlying data source (they return new streams or results).
Stream Operations
Stream operations are divided into two main categories:
1. Intermediate Operations
- These return a new stream and are lazy. They are chained together and executed when a terminal operation is called.
- Common intermediate operations:
filter(): Filters elements based on a predicate.map(): Transforms each element of the stream using a function.sorted(): Sorts the elements of the stream.distinct(): Removes duplicate elements.limit(): Limits the number of elements in the stream.skip(): Skips the firstnelements.
2. Terminal Operations
- These trigger the execution of all intermediate operations and return a result.
- Common terminal operations:
forEach(): Applies an action to each element of the stream.collect(): Collects the result of the stream into a collection (e.g.,List,Set, etc.).reduce(): Reduces the stream to a single value using a binary operator (e.g., sum of elements).count(): Returns the count of elements.findFirst()/findAny(): Returns the first or any element.allMatch()/anyMatch()/noneMatch(): Checks if all, any, or none of the elements match a given predicate.
Example Usage
1. Filtering and Mapping
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Filter names starting with "A" and convert to uppercase
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames); // Output: [ALICE]
2. Reducing a Stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Sum of elements using reduce()
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // Output: 15
3. Sorting and Limiting
List<Integer> numbers = Arrays.asList(3, 2, 4, 1, 5);
// Get top 3 smallest numbers
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.limit(3)
.collect(Collectors.toList());
System.out.println(sortedNumbers); // Output: [1, 2, 3]
Most Commonly Used Stream Commands
filter(Predicate): Filters elements based on a condition.
- Example:
filter(n -> n > 10)
map(Function): Transforms elements into another form.
- Example:
map(n -> n * 2)
collect(Collector): Collects elements into a collection likeList,Set, orMap.
- Example:
collect(Collectors.toList())
forEach(Consumer): Performs an action on each element.
- Example: forEach(System.out::println)
reduce(BinaryOperator): Reduces the elements into a single value.
- Example:
reduce(0, (a, b) -> a + b)
sorted(): Sorts the elements either naturally or by a custom comparator.
- Example:
sorted(Comparator.naturalOrder())
distinct(): Removes duplicates from the stream.
- Example:
distinct()
findFirst()andfindAny(): Finds the first or any element.
- Example:
findFirst().orElse("None")
count(): Counts the number of elements.
- Example:
count()
allMatch(),anyMatch(),noneMatch(): Tests conditions on elements.
- Example:
allMatch(n -> n > 0)
Example Workflow: Filtering, Mapping, and Collecting
List<String> words = Arrays.asList("apple", "banana", "cherry", "apple");
// Filter words longer than 5 characters, make them uppercase, and collect to a set
Set<String> result = words.stream()
.filter(word -> word.length() > 5)
.map(String::toUpperCase)
.collect(Collectors.toSet());
System.out.println(result); // Output: [BANANA, CHERRY]
Parallel Streams
Java also provides parallel streams for multi-threaded processing, improving performance in some scenarios (e.g., large data sets):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Use parallelStream for parallel processing
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);
Collectors
In addition to the commonly used Collectors.toList() and Collectors.toSet(), Java’s Collectors class offers several other useful methods that can be applied to Streams for a variety of purposes. These collectors help in performing tasks such as joining elements, grouping, partitioning, or computing statistics.
Here are some useful collectors that you can leverage:
| Collector | Description |
|---|---|
toList() | Collects elements into a List. |
toSet() | Collects elements into a Set. |
toMap() | Collects elements into a Map. |
joining() | Joins elements into a String. |
counting() | Counts elements in the stream. |
summingInt() | Sums integer values in the stream. |
averagingInt() | Averages integer values in the stream. |
maxBy() / minBy() | Finds the maximum or minimum element. |
groupingBy() | Groups elements by a classifier function. |
partitioningBy() | Partitions elements based on a predicate. |
reducing() | Performs a reduction on the elements. |
toCollection() | Collects elements into a specific collection. |
summarizingInt() | Provides statistics on elements (count, sum, min, avg, max). |
Key Points to Remember for Interviews:
- Streams are immutable: They do not modify the original source; they return a new stream or result.
- Streams are lazy: Intermediate operations like
filter()andmap()are not executed until a terminal operation is invoked. - Efficiency: Because of lazy evaluation, streams can improve performance by reducing unnecessary computations.
- Parallelism: Parallel streams can be used to process data in multiple threads for performance gains in the right scenarios.