Streams¶
- Terminal operations in Java Streams are the operations that produce a final result or a side-effect
- Intermediate operations in Java Streams allow you to transform or filter the elements of a stream in a lazy manner.

-
Streams are wrappers around a data source, allowing us to operate with that data source and making bulk processing convenient and fast.
-
A stream does not store data and, in that sense, is not a data structure. It also never modifies the underlying data source.
-
Streams don't change the original data structure, they only provide the result as per the pipelined methods.
-
Each intermediate operation is lazily executed and returns a stream as a result, hence various intermediate operations can be pipelined. Terminal operations mark the end of the stream and return the result.
-
A stream pipeline consists of a stream source, followed by zero or more intermediate operations, and a terminal operation.
-
Computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.
-
All intermediate operations are lazy, so they're not executed until a result of a processing is actually needed.
Streams offer several advantages over traditional iteration approaches in Java programming:
-
Declarative Style: Streams allow you to express your data processing logic in a more declarative and functional style. This leads to more concise, readable, and expressive code.
-
Method Chaining: Streams support method chaining, enabling you to perform multiple operations (such as filtering, mapping, and reducing) in a single pipeline. This promotes code organization and reduces the need for intermediate variables.
-
Lazy Evaluation: Stream operations are lazy by default, meaning they only process elements as needed. This can lead to improved performance, especially when working with large or infinite data sets, as unnecessary computation is avoided.
-
Parallelism: Streams can easily be parallelized to leverage multi-core processors, using the parallel() method. This allows for concurrent processing of elements, potentially improving throughput and reducing execution time.
-
Immutable State: Streams operate on immutable data, meaning they do not modify the original source data. This promotes thread safety and reduces the likelihood of unintended side effects.
-
Separation of Concerns: Streams separate the definition of data processing operations from the mechanics of iteration. This separation of concerns enhances modularity, testability, and maintainability of your code.
-
Functional Programming Constructs: Streams provide a range of functional programming constructs such as map, filter, reduce, and forEach, which align well with modern programming paradigms and facilitate functional programming techniques.
-
Support for Sequential and Parallel Processing: Streams provide a consistent API for sequential and parallel processing, allowing you to switch between the two modes seamlessly without changing your code's logic.
-
Integration with Existing APIs: Streams integrate seamlessly with existing Java APIs, such as Collections, I/O operations, and concurrency utilities, allowing for interoperability and code reuse.
Overall, streams enable more efficient, concise, and expressive data processing in Java, making it easier to write robust, scalable, and maintainable applications.
Stream Creation¶
In Java, you can create streams from various data sources, including collections, arrays, I/O resources, and numerical ranges
- Stream.builder() When builder is used, the desired type should be additionally specified in the right part of the statement, otherwise the build() method will create an instance of the Stream
Stream
- From a Collection
- Collection.stream(): Creates a stream from a collection.
- Collection.parallelStream(): Creates a parallel stream from a collection.
List
- From an Array
- Arrays.stream(array): Creates a stream from an array.
- Arrays.stream(array, fromIndex, toIndex): Creates a stream from a subarray.
String[] array = { "apple", "banana", "cherry" };
Stream
- From Static Factory Methods
- Stream.of(T... values): Creates a stream from the specified values.
- Stream.empty(): Creates an empty stream.
Stream
- From I/O Resources
- Files.lines(Path): Creates a stream of lines from a file.
- BufferedReader.lines(): Creates a stream of lines from a buffered reader.
try (Stream
- From Primitive Arrays
- IntStream.range(int startInclusive, int endExclusive): Creates a stream of integers within the specified range.
- LongStream.range(long startInclusive, long endExclusive): Creates a stream of longs within the specified range.
- DoubleStream.of(double... values): Creates a stream of doubles from the specified values.
IntStream intStream = IntStream.range(1, 10);
- From Iterative Constructs
- Stream.iterate(T seed, UnaryOperator
f): Creates an infinite stream where each element is generated by applying the given function to the previous element. - Stream.generate(Supplier
s): Creates an infinite stream where each element is generated by invoking the given supplier.
Stream
- Stream.generate()
The generate() method accepts a Supplier
for element generation. As the resulting stream is infinite, the developer should specify the desired size, or the generate() method will work until it reaches the memory limit:
Stream
Intermediate operations¶
Intermediate operations in Java Streams allow you to transform or filter the elements of a stream in a lazy manner.
- filter(Predicate) Filters the elements of the stream based on a given predicate.
stream.filter(x -> x > 5)
- map(Function)
Transforms each element of the stream using the provided function. stream.map(x -> x * x)
- flatMap(Function) Flattens the elements of nested streams into a single stream.
stream.flatMap(list -> list.stream())
- distinct()
Removes duplicate elements from the stream. stream.distinct()
-
sorted() Sorts the elements of the stream in natural order. stream.sorted()
-
sorted(Comparator) Sorts the elements of the stream using the provided comparator. stream.sorted((a, b) -> a.compareTo(b))
-
limit(long): Limits the number of elements in the stream to the specified maximum size. stream.limit(10)
-
skip(long): Skips the specified number of elements from the beginning of the stream. stream.skip(5)
-
peek(Consumer): Allows performing a side-effect operation on each element of the stream without changing the elements themselves.
stream.peek(System.out::println)
- takeWhile(Predicate) (Java 9+):
Takes elements from the stream while the predicate holds true. stream.takeWhile(x -> x < 5)
- dropWhile(Predicate) (Java 9+):
Drops elements from the stream while the predicate holds true, and returns the rest. stream.dropWhile(x -> x < 5)
- unordered():
Disables the ordering of elements in the stream. stream.unordered()
Terminal operations¶
Terminal operations in Java Streams are the operations that produce a final result or a side-effect.
-
forEach(Consumer) Performs an action for each element of the stream. stream.forEach(System.out::println)
-
collect(Collector)
Accumulates the elements of the stream into a collection or other data structure.
List
- count()
Returns the count of elements in the stream as a long. long count = stream.count()
- anyMatch(Predicate)
Returns true if any element of the stream matches the given predicate; otherwise, returns false. boolean anyMatch = stream.anyMatch(x -> x > 5)
- allMatch(Predicate)
Returns true if all elements of the stream match the given predicate; otherwise, returns false. boolean allMatch = stream.allMatch(x -> x > 0)
- noneMatch(Predicate): Returns true if none of the elements of the stream match the given predicate; otherwise, returns false.
boolean noneMatch = stream.noneMatch(x -> x < 0)
- findFirst() Returns an Optional containing the first element of the stream, or an empty Optional if the stream is empty.
Optional
- findAny() Returns an Optional containing any element of the stream, or an empty Optional if the stream is empty.
Optional
- reduce(BinaryOperator) Combines the elements of the stream using the specified binary operator.
int sum = stream.reduce(0, (a, b) -> a + b)
- min(Comparator) Returns the minimum element of the stream according to the provided comparator, or an empty Optional if the stream is empty.
Optional
-
max(Comparator): Returns the maximum element of the stream according to the provided comparator, or an empty Optional if the stream is empty. Optional
max = stream.max(Comparator.naturalOrder()) -
toArray(): Collects the elements of the stream into an array.
String[] array = stream.toArray(String[]::new)
These terminal operations trigger the evaluation of the stream and produce a final result or perform a side-effect. Once a terminal operation is called on a stream, the stream cannot be reused.
Java Collectors¶
Collectors is one of the utility class in JDK which contains a lot of utility functions. It is mostly used with Stream API as a final step.
| Collectors | Example |
|---|---|
| toList() | List |
| toSet() | List |
| toCollection() | List |
| Counting() | List |
| toMap() | List |
| joining() | List |
| groupingBy() | List |
| minBy() | List |
| maxBy() | List |
| partitioningBy() | List |
| toUnmodifiableList() | List |
| toUnmodifiableSet() | List |
| averagingLong() | List |
| averagingInt() | List |
| averagingDouble() | List |
| summarizingInt() | List |
| summingInt() | List |
| summingDouble() | List |
| summingLong() | List |
Comparisons¶
| Advantages of Streams | Advantages of Loops |
|---|---|
| Streams are a more declarative style. Or a more expressive style. | Performance: A for loop through an array is extremely lightweight both in terms of heap and CPU usage. If raw speed and memory thriftiness is a priority, using a stream is worse. |
| Streams have a strong affinity with functions. Java 8 introduces lambdas and functional interfaces, which opens a whole toolbox of powerful techniques. Streams provide the most convenient and natural way to apply functions to sequences of objects. | Familiarity: The world is full of experienced procedural programmers, from many language backgrounds, for whom loops are familiar and streams are novel. In some environments, you want to write code that's familiar to that kind of person. |
| Streams encourage less mutability. This is sort of related to the functional programming aspect i.e., the kind of programs we write using streams tend to be the kind of programs where we don't modify objects. | Cognitive overhead: Because of its declarative nature, and increased abstraction from what's happening underneath, you may need to build a new mental model of how code relates to execution. Actually, you only need to do this when things go wrong, or if you need to deeply analyze performance or subtle bugs. When it "just works", it just works. |
| Streams encourage loose coupling. Our stream-handling code doesn't need to know the source of the stream or its eventual terminating method. | Debuggers are improving, but even now, when we are stepping through stream code in a debugger, it can be harder work than the equivalent loop, because a simple loop is very close to the variables and code locations that a traditional debugger works with. |
| Streams can succinctly express quite sophisticated behavior. |
| Sequential Stream | Parallel Stream |
|---|---|
| Runs on a single-core of the computer | Utilize the multiple cores of the computer. |
| Performance is poor | The performance is high. |
| Order is maintained | Doesn't care about the order, |
| Only a single iteration at a time just like the for-loop. | Operates multiple iterations simultaneously in different available cores. |
| Each iteration waits for currently running one to finish, | Waits only if no cores are free or available at a given time, |
| More reliable and less error, | Less reliable and error-prone. |
| Platform independent, | Platform dependent |
Questions¶
- What is a Stream in Java?
- Answer: A Stream in Java is a sequence of elements supporting sequential and parallel aggregate operations. It is not a data structure but a view of the data that can be manipulated using functional-style operations.
- How do you create a Stream in Java?
- Answer: Streams can be created from various sources, such as Collections, Arrays, or I/O channels. Examples include Stream.of(), Collection.stream(), Arrays.stream(), and Files.lines().
- What are the differences between a Stream and a Collection?
-
Answer: Collections store data, while Streams are used to process data. Collections are eagerly constructed, whereas Streams are lazily constructed and can be infinite. Streams do not store elements; instead, they convey elements from a source to a pipeline of operations.
-
Explain the difference between intermediate and terminal operations in a Stream.
- Answer: Intermediate operations return a new Stream and are lazy, meaning they are not executed until a terminal operation is invoked. Examples include filter(), map(), and sorted(). Terminal operations produce a result or a side effect and trigger the processing of the stream. Examples include forEach(), collect(), and reduce().
- What is the purpose of the filter() method in a Stream?
- Answer: The filter() method is an intermediate operation that allows you to exclude elements from a Stream based on a given predicate. Only elements that satisfy the predicate are included in the resulting Stream.
- How does the map() method work in a Stream?
- Answer: The map() method is an intermediate operation that applies a function to each element of the Stream, transforming each element into another form, and returns a new Stream with the transformed elements.
- What is the difference between map() and flatMap()?
- Answer: map() applies a function to each element and returns a Stream of the results.
-
flatMap() applies a function that returns a Stream for each element and then flattens the resulting Streams into a single Stream. flatMap() is used for transforming and flattening elements.
-
What is the purpose of the reduce() method, and how does it work?
- Answer: The reduce() method is a terminal operation that performs a reduction on the elements of the Stream using an associative accumulation function and returns an Optional. It combines elements of the Stream to produce a single result, such as summing a list of numbers.
- How do you handle parallel processing with Streams?
- Answer: Parallel processing can be achieved by calling the parallelStream() method on a Collection or the parallel() method on a Stream. This divides the Stream's operations across multiple threads to improve performance for large datasets.
- What is the purpose of the collect() method in a Stream?
- Answer: The collect() method is a terminal operation used to transform the elements of a Stream into a different form, such as a List, Set, Map, or another collection. It takes a Collector that specifies how to accumulate the elements.
- Can you explain the concept of lazy evaluation in Streams?
- Answer: Lazy evaluation means that intermediate operations on a Stream are not executed until a terminal operation is invoked. This allows Streams to be more efficient, as intermediate operations can be optimized and combined.
- What are the benefits of using Streams in Java?
- Answer: Benefits include concise and readable code, easier parallelism, improved productivity with functional-style operations, and the ability to focus on the what rather than the how of data processing.
Practical Questions¶
Write a Stream operation to find the first even number greater than 10 in a list of integers.
List
Given a list of names, write a Stream operation to filter names starting with 'A' and collect them into a List.
List
How would you use Streams to count the number of unique words in a text file? try { long uniqueWordCount = Files.lines(Paths.get("file.txt")) .flatMap(line -> Arrays.stream(line.split("\\s+"))) .distinct() .count(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
How do you convert a list of integers to a list of their squares using Streams?
List
How do you find the maximum and minimum value in a list of integers using Streams?
List<Integer> numbers = Arrays.asList(5, 10, -2, 23, 7);
int max = numbers.stream()
.max(Integer::compare)
.orElseThrow(NoSuchElementException::new);
int min = numbers.stream()
.min(Integer::compare)
.orElseThrow(NoSuchElementException::new);
How do you join a list of strings into a single string separated by commas using Streams?
List<String> strings = Arrays.asList("apple", "banana", "cherry");
String result = strings.stream()
.collect(Collectors.joining(", "));
How do you group a list of strings by their length using Streams?
List<String> strings = Arrays.asList("apple", "banana", "cherry", "date");
Map<Integer, List<String>> groupedByLength = strings.stream()
.collect(Collectors.groupingBy(String::length));
How do you count the occurrences of each word in a list of strings using Streams?
List<String> words = Arrays.asList("apple", "banana", "apple", "cherry", "banana", "apple");
Map<String, Long> wordCount = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
How do you partition a list of numbers into even and odd using Streams?
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
How do you find the sum of all even numbers in a list using Streams?
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sumOfEvens = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
How do you flatMap a list of lists into a single list using Streams?
List<List<Integer>> listOfLists = Arrays.asList(Arrays.asList(1, 2, 3), Arrays.asList(4, 5), Arrays.asList(6, 7, 8));
List<Integer> flatList = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
How do you find the second highest number in a list using Streams?
List<Integer> numbers = Arrays.asList(5, 10, 15, 20, 25);
int secondHighest = numbers.stream()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst()
.orElseThrow(NoSuchElementException::new);
How do you collect the distinct elements of a list and sort them in natural order using Streams?
List<Integer> numbers = Arrays.asList(5, 3, 2, 8, 3, 1, 2, 5);
List<Integer> distinctSorted = numbers.stream()
.distinct()
.sorted()
.collect(Collectors.toList());
How do you generate a stream of the first 10 Fibonacci numbers using Streams?
List<Integer> fibonacci = Stream.iterate(new int[]{0, 1}, f -> new int[]{f[1], f[0] + f[1]})
.limit(10)
.map(f -> f[0])
.collect(Collectors.toList());
How do you convert a list of strings to a map with the string as the key and its length as the value using Streams?
List<String> strings = Arrays.asList("apple", "banana", "cherry");
Map<String, Integer> stringLengthMap = strings.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
How do you remove duplicates from a list of integers using Streams?
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
How do you check if any string in a list starts with a given prefix using Streams?
List<String> strings = Arrays.asList("apple", "banana", "cherry");
boolean anyStartsWithA = strings.stream()
.anyMatch(s -> s.startsWith("a"));
How do you find the average length of strings in a list using Streams?
List<String> strings = Arrays.asList("apple", "banana", "cherry");
double averageLength = strings.stream()
.mapToInt(String::length)
.average()
.orElse(0.0);
References¶
https://www.geeksforgeeks.org/difference-between-stream-of-and-arrays-stream-method-in-java/
https://www.baeldung.com/java-parallelstream-vs-stream-parallel
https://medium.com/@vino7tech/difference-between-stream-and-parallel-stream-in-java-8-0c20004706d2
https://www.geeksforgeeks.org/difference-between-stream-of-and-arrays-stream-method-in-java/
https://medium.com/@javageeksociety/arrays-stream-vs-stream-of-in-java-8-be7d6b757e4e
Related: See Functional Programming for lambdas, method references, and functional interfaces used in Stream operations.