The Java 8 release marked a significant milestone in the language's evolution. Among its most impactful features is the Stream API, which works seamlessly with Lambda expressions to revolutionize how we manipulate collections. This combination provides elegant, functional-style operations that significantly reduce boilerplate code while improving readability.
So, what exactly is a Stream?
A Stream represents a pipeline for processing sequences of elements. Think of it as a continuous flow of data where you can apply various operations through the Stream API. These operations include filtering, sorting, and aggregation—transforming raw data into meaningful results with out modifying the original data source.
Streams can be created from arrays or collections, and operations fall into two distinct categories:
- Intermediate operations: These return a new Stream, allowing chaining of multiple operations in a fluent manner.
- Terminal operations: Each Stream can only have one terminal operation, which triggers the actual processing and produces a result (either a collection or a scalar value). Once executed, the Stream cannot be reused.
Key characteristics of Streams include:
- Streams do not store data—they compute results based on specified rules and output the final result.
- Streams do not modify the original data source; instead, they typically generate new collections or values.
- Streams employ lazy evaluation, meaning intermediate operations are only executed when a terminal operation is invoked.
2 Creating Streams
The Stream API provides multiple ways to create streams from existing data structures.
2.1 Creating Streams from Collections
Using the stream() method from java.util.Collection interface:
List<string> items = Arrays.asList("x", "y", "z");
// Creates a sequential stream
Stream<string> sequentialStream = items.stream();
// Creates a parallel stream for concurrent processing
Stream<string> parallelStream = items.parallelStream();
</string></string></string>
2.2 Creating Streams from Arrays
Using the Arrays.stream() method:
int[] numbers = {2, 4, 6, 8, 10};
IntStream numberStream = Arrays.stream(numbers);
2.3 Using Static Factory Methods
The Stream class provides several static methods for stream creation:
// Creating a stream from individual elements
Stream<integer> stream1 = Stream.of(1, 2, 3, 4, 5, 6);
// Creating an infinite stream using iterate, limited to 4 elements
Stream<integer> stream2 = Stream.iterate(0, n -> n + 2).limit(4);
stream2.forEach(System.out::println);
// Creating a stream of random values
Stream<double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);
</double></integer></integer>
Output:
0
2
4
6
0.8472189027166124
0.3085084954697114
0.957045261725583
2.4 Sequential vs Parallel Streams
A sequential stream processes elements in order using a single thread. A parallel stream, conversely, divides the workload across multiple threads, significantly improving performance for large datasets when element order doesn't matter.
You can also convert a sequential stream to parallel:
Optional<integer> result = list.stream()
.parallel()
.filter(n -> n > 50)
.findFirst();
</integer>
3 Working with Streams
Before diving into operations, let's understand Optional. This class is a container that may or may not hold a value. When a value is present, isPresent() returns true, and get() retrieves the value. This prevents null pointer exceptions and provides a cleaner approach to handling potential absent values.
Now let's explore Stream operations through comprehensive examples.
Sample Data Model
We'll use an Employee class throughout our examples:
List<employee> employeeRoster = new ArrayList<>();
employeeRoster.add(new Employee("Sarah", 8500, 28, "female", "Boston"));
employeeRoster.add(new Employee("Michael", 7200, 32, "male", "Chicago"));
employeeRoster.add(new Employee("Emily", 9100, 26, "female", "Boston"));
employeeRoster.add(new Employee("David", 6800, 30, "male", "Seattle"));
employeeRoster.add(new Employee("James", 10500, 35, "male", "Boston"));
employeeRoster.add(new Employee("Olivia", 7900, 29, "female", "Chicago"));
class Employee {
private String name;
private int salary;
private int age;
private String gender;
private String city;
public Employee(String name, int salary, int age, String gender, String city) {
this.name = name;
this.salary = salary;
this.age = age;
this.gender = gender;
this.city = city;
}
// Getters and setters omitted for brevity
}
</employee>
3.1 Traversal and Matching
Streams support traversal and element matching similar to collections, with results wrapped in Optional containers:
public class StreamTraversalDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(8, 5, 10, 2, 7, 4, 12);
// Filter and print elements greater than 6
numbers.stream()
.filter(n -> n > 6)
.forEach(System.out::println);
// Find the first matching element
Optional<integer> firstMatch = numbers.stream()
.filter(n -> n > 6)
.findFirst();
// Find any matching element (useful in parallel streams)
Optional<integer> anyMatch = numbers.parallelStream()
.filter(n -> n > 6)
.findAny();
// Check if any element matches the condition
boolean hasMatch = numbers.stream()
.anyMatch(n -> n > 6);
System.out.println("First match: " + firstMatch.orElse(-1));
System.out.println("Any match: " + anyMatch.orElse(-1));
System.out.println("Contains elements > 6: " + hasMatch);
}
}
</integer></integer></integer>
3.2 Filtering Elements
Filtering extracts elements that satisfy a specified condition into a new stream:
public class StreamFilterDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(5, 8, 2, 10, 1, 7);
numbers.stream()
.filter(n -> n > 5)
.forEach(System.out::println);
}
}
</integer>
Output: 8 10 7
Practical Example: Filter employees with salary exceeding 8000 and collect them into a new list:
public class EmployeeFilterDemo {
public static void main(String[] args) {
List<employee> employeeRoster = new ArrayList<>();
// ... populate list ...
List<employee> highEarners = employeeRoster.stream()
.filter(e -> e.getSalary() > 8000)
.collect(Collectors.toList());
highEarners.forEach(e -> System.out.println(e.getName()));
}
}
</employee></employee>
3.3 Aggregation Operations
Common aggregation functions include finding maximum, minimum, and counting elements:
public class StreamAggregationDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(4, 9, 2, 7, 5, 11, 8);
// Find the maximum value
Optional<integer> max = numbers.stream()
.max(Integer::compareTo);
// Find the minimum value
Optional<integer> min = numbers.stream()
.min(Integer::compareTo);
// Count total elements
long count = numbers.stream()
.filter(n -> n > 5)
.count();
// Employee salary calculations
List<employee> employees = new ArrayList<>();
// ... populate list ...
Optional<integer> highestSalary = employees.stream()
.map(Employee::getSalary)
.max(Integer::compareTo);
Double averageSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.average()
.orElse(0.0);
Integer totalSalary = employees.stream()
.mapToInt(Employee::getSalary)
.sum();
System.out.println("Maximum: " + max.orElse(0));
System.out.println("Minimum: " + min.orElse(0));
System.out.println("Count > 5: " + count);
System.out.println("Highest salary: " + highestSalary.orElse(0));
System.out.println("Average salary: " + averageSalary);
System.out.println("Total salary: " + totalSalary);
}
}
</integer></employee></integer></integer></integer>
3.4 Mapping Operations
Mapping transforms each element in the stream using a provided function:
public class StreamMappingDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(3, 6, 9, 12, 15);
// Square each number
numbers.stream()
.map(n -> n * n)
.forEach(System.out::println);
// Extract employee names
List<employee> employees = new ArrayList<>();
// ... populate list ...
List<string> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
// Use flatMap to flatten nested structures
List<list>> nestedCollections = Arrays.asList(
Arrays.asList("Apple", "Banana"),
Arrays.asList("Cherry", "Date")
);
List<string> flattened = nestedCollections.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
}
</string></list></string></employee></integer>
3.5 Reduction Operations
Reduction combines stream elements in to a single value using an accumulator function:
public class StreamReductionDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Sum all numbers using reduce with identity
Integer sum = numbers.stream()
.reduce(0, Integer::sum);
// Find product using reduce
Integer product = numbers.stream()
.reduce(1, (a, b) -> a * b);
// Concatenate strings
List<string> words = Arrays.asList("Hello", " ", "World", "!");
String sentence = words.stream()
.reduce("", String::concat);
// Maximum salary reduction
List<employee> employees = new ArrayList<>();
// ... populate list ...
Optional<employee> topEarner = employees.stream()
.reduce((e1, e2) ->
e1.getSalary() > e2.getSalary() ? e1 : e2);
System.out.println("Sum: " + sum);
System.out.println("Product: " + product);
System.out.println("Sentence: " + sentence);
}
}
</employee></employee></string></integer>
3.6 Collecting Results
The collect() method transforms stream elements into various collection types or other representations.
3.6.1 Converting to Collections
public class StreamCollectionDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
// Convert to List
List<integer> asList = numbers.stream()
.collect(Collectors.toList());
// Convert to Set (removes duplicates)
Set<integer> asSet = numbers.stream()
.collect(Collectors.toSet());
// Convert to Map
List<employee> employees = new ArrayList<>();
// ... populate list ...
Map<string integer=""> nameToSalary = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalary
));
// LinkedHashMap preserves insertion order
Map<string integer=""> orderedMap = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getSalary,
(existing, replacement) -> existing,
LinkedHashMap::new
));
}
}
</string></string></employee></integer></integer></integer>
3.6.2 Statistical Summaries
public class StreamStatisticsDemo {
public static void main(String[] args) {
List<employee> employees = new ArrayList<>();
// ... populate list ...
// Counting
Long employeeCount = employees.stream()
.collect(Collectors.counting());
// Averaging
Double avgSalary = employees.stream()
.collect(Collectors.averagingInt(Employee::getSalary));
// Summarizing statistics
IntSummaryStatistics salaryStats = employees.stream()
.collect(Collectors.summarizingInt(Employee::getSalary));
System.out.println("Count: " + employeeCount);
System.out.println("Average: " + avgSalary);
System.out.println("Statistics: " + salaryStats);
// Output: IntSummaryStatistics{count=6, sum=48300, min=6800, average=8050.0, max=10500}
}
}
</employee>
3.6.3 Grouping Data
public class StreamGroupingDemo {
public static void main(String[] args) {
List<employee> employees = new ArrayList<>();
// ... populate list ...
// Group by gender
Map<string list="">> byGender = employees.stream()
.collect(Collectors.groupingBy(Employee::getGender));
// Group by city and gender combined
Map<string list="" map="">>> byCityAndGender =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getCity,
Collectors.groupingBy(Employee::getGender)
));
// Partition by salary threshold
Map<boolean list="">> partitionedBySalary =
employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 8000));
// Group by with counting
Map<string long=""> genderCounts = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.counting()
));
}
}
</string></boolean></string></string></employee>
3.6.4 String Joining
public class StreamJoiningDemo {
public static void main(String[] args) {
List<string> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// Simple joining
String joined = names.stream()
.collect(Collectors.joining());
// Joining with delimiter
String delimited = names.stream()
.collect(Collectors.joining(", "));
// Joining with prefix and suffix
String formatted = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// Building employee roster string
List<employee> employees = new ArrayList<>();
// ... populate list ...
String employeeDirectory = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(" | "));
System.out.println("Simple: " + joined);
System.out.println("Delimited: " + delimited);
System.out.println("Formatted: " + formatted);
}
}
</employee></string>
3.6.5 Advanced Reduction
public class StreamAdvancedReductionDemo {
public static void main(String[] args) {
List<employee> employees = new ArrayList<>();
// ... populate list ...
// Total salary using reducing
Integer totalSalary = employees.stream()
.map(Employee::getSalary)
.collect(Collectors.reducing(0, Integer::sum));
// Highest paid employee using reducing
Optional<employee> topPerformer = employees.stream()
.collect(Collectors.reducing(
(e1, e2) ->
e1.getSalary() > e2.getSalary() ? e1 : e2
));
// Salary bonus calculation
Double totalWithBonus = employees.stream()
.collect(Collectors.reducing(
0.0,
Employee::getSalary,
(subtotal, salary) -> subtotal + salary * 0.10
));
}
}
</employee></employee>
3.7 Sorting Operations
Streams can sort elements naturally or using custom comparators:
public class StreamSortingDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
// Natural order sorting
List<integer> sortedAsc = numbers.stream()
.sorted()
.collect(Collectors.toList());
// Descending order sorting
List<integer> sortedDesc = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// Custom object sorting
List<employee> employees = new ArrayList<>();
// ... populate list ...
// Sort by salary descending, then by age ascending for same salaries
List<employee> sortedEmployees = employees.stream()
.sorted(Comparator
.comparingInt(Employee::getSalary).reversed()
.thenComparingInt(Employee::getAge))
.collect(Collectors.toList());
// Case-insensitive string sorting
List<string> names = Arrays.asList("alice", "CHARLIE", "bob", "Diana");
List<string> caseInsensitive = names.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
}
}
</string></string></employee></employee></integer></integer></integer>
3.8 Limiting and Skipping
You can extract subsets of stream elements:
public class StreamLimitSkipDemo {
public static void main(String[] args) {
List<integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Get first 5 elements
List<integer> firstFive = numbers.stream()
.limit(5)
.collect(Collectors.toList());
// Skip first 3 elements
List<integer> remaining = numbers.stream()
.skip(3)
.collect(Collectors.toList());
// Skip first 2, then take next 3
List<integer> middle = numbers.stream()
.skip(2)
.limit(3)
.collect(Collectors.toList());
// Find top 3 highest-paid employees
List<employee> employees = new ArrayList<>();
// ... populate list ...
List<employee> topEarners = employees.stream()
.sorted(Comparator.comparingInt(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
}
}
</employee></employee></integer></integer></integer></integer>
4 Practical Use Cases
Here are common real-world scenarios where Stream API excels:
Scenario 1: Filter employees earning above 8000 into a new collection
List<employee> filteredStaff = employeeRoster.stream()
.filter(e -> e.getSalary() > 8000)
.collect(Collectors.toList());
</employee>
Scenario 2: Calculate maximum salary, average salary, and total compensation
IntSummaryStatistics salarySummary = employeeRoster.stream()
.collect(Collectors.summarizingInt(Employee::getSalary));
int maximum = salarySummary.getMax();
double average = salarySummary.getAverage();
long total = salarySummary.getSum();
Scenario 3: Sort employees by salary descending, then by age ascending for equal salaries
List<employee> ranking = employeeRoster.stream()
.sorted(Comparator
.comparingInt(Employee::getSalary).reversed()
.thenComparingInt(Employee::getAge))
.collect(Collectors.toList());
</employee>
Scenario 4: Group employees by gender, by city, or by salary threshold
// Single classification
Map<string list="">> byGender = employeeRoster.stream()
.collect(Collectors.groupingBy(Employee::getGender));
// Multi-level grouping
Map<string list="" map="">>> demographicGroups =
employeeRoster.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.groupingBy(Employee::getCity)
));
// Binary partition
Map<boolean list="">> segmented = employeeRoster.stream()
.collect(Collectors.partitioningBy(
e -> e.getSalary() > 8000
));
</boolean></string></string>
While these operations can be accomplished using traditional iteration, the Stream API produces code that is more concise, readable, and maintainable. The functional approach also reduces the likelihood of common errors such as off-by-one bugs or improper loop termination.