Java Stream API was introduced in java 8, is a powerful tool for processing sequences of elements in a functional style. It allows you to perform operations such as filtering, mapping, and reducing on collections of data. The Stream API can work with collections, arrays, and I/O channels, making it a versatile tool for modern Java development.
What is Stream?
A stream is a sequence of element that supports various methods which can be pipelined to produce the desired result. Streams are not data structures; instead, they convey elements from a source (like collections) though a pipeline of computations operations.

Characteristics of Streams
- No Storage: Streams do not store elements; they are computed on demand.
- Functional: Operations on streams do not modify the underlying data structure; they produce a new stream or result.
- Laziness: Many stream operations are lazy, meaning they not executed until a terminal operation is invoked.
- Possibly Infinite: Streams can be finite or potentially infinite, depending on the source.
Creating Streams
Streams can be created from various data sources such as collections, arrays, or I/O channels
From Collections
List<String> myList = Arrays.asList("a", "b", "c", "d");
Stream<String> myStream = myList.stream();
From Arrays
String[] myArray = {"a", "b", "c", "d"};
Stream<String> myStream = Arrays.stream(myArray);
From Values
Stream<String> myStream = Stream.of("a", "b", "c", "d");
From Files
Path path = Paths.get("file.txt");
Stream<String> lines = Files.lines(path);
Infinite Streams
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2);
Termination Operation
Collect
List<String> resultList = myStream.collect(Collectors.toList());
forEach
myStream.forEach(System.out::println);
Reduce
Optional<String> concatenated = myStream.reduce((s1, s2) -> s1 + s2);
Count
long count = myStream.count();
FindFirst
Optional<String> firstElement = myStream.findFirst();
Stream API in Action: Examples
Example 1: Filter and Collect
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
This will filter all names and return filteredNames = [“John”, “Jane”, “Jack”]
Example 2: Map and Reduce
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.map(n -> n * n)
.reduce(0, Integer::sum);
This will combine all elements to single result. In this case above streams will transform each element to its square [1, 4, 9, 16, 25]. After that will plus each element together with the start value 0, the result will be 55
Example 3: Grouping By
List<String> items = Arrays.asList("apple", "banana", "apple", "orange", "banana", "banana");
Map<String, Long> itemCount = items.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
This will group and count each element in an arrays {orange=1, banana=3, apple=2}
Example 4: Parallel Streams
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream()
.forEach(System.out::println);
This will execute in parallel. We can see it with the result not be ordered [3,5,4,2,1]
Advanced Stream Concepts
Infinite Streams
An Infinite stream is a stream that can potentially produce an unbounded number of elements. Such streams typically generated using methods like `Stream.iterate` or `Stream.generate`. These methods allow you to define a potentially infinite series of elements, which can be processed lazily.
Stream.iterate
The Stream.iterate method generates an infinite sequential ordered stream produced by iterative application of a function to an initial element, typically used to produce a sequence of elements
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
- seed: The initial element
- f: A function to be applied to the previous element to produce a new element
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
infiniteStream.limit(10).forEach(System.out::println);
This will generate an array with size of 10 and next element generate by current + 1 so result will be [0, 1,2,3,4,5,6,7,8,9]
Custom Collectors
The Collector Interface
A Collector in java is a mutable reduction operation that accumulates input elements into a mutable container, optionally transforming the accumulated result into a final representation after all input elements have been processed. The Collector interface provides methods to create such collectors.
Component of a Custom Collector
The `Collector.of` method is a factory method that creates a new collector with three or four functions
- Supplier: Creates a new result container
- Accumulator: Incorporates a new element into the result container
- Combiner: Merges two result containers
- Finisher: Transform the final result container into the desired result type
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> myStream = list.stream();
Collector<String, StringJoiner, String> customCollector =
Collector.of(
() -> new StringJoiner(", "),
(j, s) -> j.add(s.toUpperCase()),
StringJoiner::merge,
StringJoiner::toString
);
String result = myStream.collect(customCollector);
System.out.println(result);
Detailed Breakdown
- Supplier: () -> new StringJoiner(“, “)
- This will create new StringJoiner with a comma and space (“, “). This provides the initial result container.
- Accumulator: (j, s) -> j.add(s.toUpperCase())
- This lambda expression takes a StringJoiner (j) and a string (s), converts s to uppercase, and adds it to the StringJoiner
- Combiner: StringJoiner::merge
- The combiner function merges two StringJoiner instances, which is important for parallel stream processing where the stream is divided into substreams.
- Finisher: StringJoiner::toString
- The finisher function converts the StringJoiner into its final string representation.
Output
APPLE, BANANA, CHERRY
FlatMap
The flatmap method is used to flatten a stream of streams into a single stream
List<List<String>> list = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d"));
Stream<String> flatStream = list.stream()
.flatMap(Collection::stream);
flatStream.forEach(System.out::println);
Here we have a list [[“a”, “b”], [“c”, “d”]] -> [“a”, “b”, “c”, “d”]
Conclusion
The Java Stream API is a powerful and flexible tool for processing data in a functional style. By understanding its concepts and utilizing its features, developer can write cleaner, more concise, and efficient code. Whether filtering data, transforming elements, or aggregating results, the Stream API provides a rich set of operations that make data manipulation straightforward and expressive.