Introduction
With Java 8, Oracle introduced several groundbreaking features, but one of the most powerful and impactful is the Stream API. It enables functional-style programming for processing sequences of elements, such as collections, arrays, or I/O channels.
Objective
The aim of this article is to provide a deep understanding of the Java 8 Stream API, including what it is, how it differs from traditional loops, and how to use its powerful features such as map
, filter
, reduce
, collect
, and more.
What is a Stream?
A Stream is not a data structure. It represents a sequence of elements supporting sequential and parallel aggregate operations. Unlike collections, Streams do not store elements. Instead, they convey elements from a source (like a list or set) through a pipeline of computational operations.
Stream vs Collection
- Collection: Stores and retrieves data.
- Stream: Describes computations on data.
- Streams can be consumed only once.
How to Create Streams
import java.util.*;
import java.util.stream.*;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream(); // Creates a sequential stream
Stream<String> parallelStream = names.parallelStream(); // Creates a parallel stream
}
}
Stream Operations
Stream operations are either intermediate or terminal.
- Intermediate: map, filter, sorted (returns another stream)
- Terminal: collect, forEach, reduce (produces a result or side-effect)
Common Stream Methods with Examples
1. forEach()
List<String> list = Arrays.asList("Java", "Python", "C++");
list.stream().forEach(System.out::println);
2. filter()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
3. map()
List<String> names = Arrays.asList("john", "jane", "doe");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
4. sorted()
List<String> fruits = Arrays.asList("Banana", "Apple", "Mango");
fruits.stream()
.sorted()
.forEach(System.out::println);
5. collect()
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
6. reduce()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum = " + sum);
Creating Streams from Different Sources
From Arrays:
int[] nums = {1, 2, 3, 4};
IntStream stream = Arrays.stream(nums);
From String:
"Hello World".chars().forEach(ch -> System.out.println((char) ch));
Stream Pipeline
A typical stream pipeline looks like this:
source -> intermediate operations -> terminal operation
Example: Filter and Collect
List<String> list = Arrays.asList("cat", "dog", "elephant");
List<String> filtered = list.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
System.out.println(filtered);
Parallel Streams
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);
System.out.println("Parallel Sum = " + sum);
Stream with Custom Objects
class Employee {
String name;
int salary;
Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
}
public class Test {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("John", 5000),
new Employee("Jane", 7000),
new Employee("Jack", 4000)
);
employees.stream()
.filter(e -> e.salary > 4500)
.map(e -> e.name)
.forEach(System.out::println);
}
}
Advanced: Grouping and Partitioning
Map<Integer, List<String>> groupedByLength =
Stream.of("Java", "Python", "JS", "C")
.collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
Best Practices
- Use streams for bulk operations on collections.
- Avoid side-effects in lambda expressions.
- Use parallel streams only when processing is CPU intensive.
- Always remember that streams are lazy and evaluated only when terminal operation is called.
When Not to Use Streams
- When you need indexed access (e.g., with arrays).
- For simple loops — stream syntax may be overkill.
- When performance tuning is critical and streams introduce overhead.
Conclusion
The Stream API in Java 8 revolutionizes how we interact with data structures in Java. By allowing you to write more declarative and functional code, it improves readability and reduces boilerplate. Through this tutorial, you’ve learned the core functionality of streams and how to apply it with real examples.