Streams in Java provide a functional appproach to processing sequences of elements, such as collections or arrays. They support operations like filtering, mapping, and reducing through a pipeline of intermediate and terminal operations, leading to more concise and efficient code.
Instantiating Streams
From a List
A stream can be created from a List.
List<String> items = List.of("apple", "banana", "cherry");
Stream<String> sequentialStream = items.stream();
Stream<String> concurrentStream = items.parallelStream();
From an Array
Arrays can also serve as a stream source.
int[] numbers = {10, 20, 30, 40, 50};
IntStream numStream = Arrays.stream(numbers);
Custom Stream Generation
Streams can be generated using static factory methods.
// Using of()
Stream<Integer> fixedStream = Stream.of(5, 10, 15);
// Using iterate()
Stream<Integer> iterativeStream = Stream.iterate(1, n -> n * 2).limit(5);
// Using generate()
Stream<Double> randomStream = Stream.generate(Math::random).limit(3);
Core Stream Operations
Searching and Iterating
Methods exist to find elements or check conditions.
List<Integer> values = List.of(3, 7, 1, 9, 4, 12);
List<Integer> filtered = values.stream().filter(v -> v > 5).collect(Collectors.toList());
Optional<Integer> firstMatch = values.stream().filter(v -> v > 5).findFirst();
Optional<Integer> anyMatchParallel = values.parallelStream().filter(v -> v > 5).findAny();
boolean exists = values.stream().anyMatch(v -> v > 5);
System.out.println("Values > 5: " + filtered);
System.out.println("First match: " + firstMatch.orElse(-1));
System.out.println("Any match: " + anyMatchParallel.orElse(-1));
System.out.println("Exists > 5: " + exists);
Filtering Elements
The filter operation selects elements based on a predicate.
record Employee(String id, String name, int salary, String department) {}
List<Employee> staff = new ArrayList<>();
staff.add(new Employee("E1", "Alice", 4500, "IT"));
staff.add(new Employee("E2", "Bob", 3200, "HR"));
staff.add(new Employee("E3", "Charlie", 2900, "IT"));
staff.add(new Employee("E4", "Diana", 5100, "Finance"));
List<String> highEarners = staff.stream()
.filter(e -> e.salary() > 3000)
.map(Employee::name)
.collect(Collectors.toList());
System.out.println("Employees earning > 3000: " + highEarners);
Aggregation Operations
Common statistical operations include min, max, count, and sum.
Optional<Employee> lowestPaid = staff.stream().min(Comparator.comparingInt(Employee::salary));
Optional<Employee> highestPaid = staff.stream().max(Comparator.comparingInt(Employee::salary));
long countHighSalary = staff.stream().filter(e -> e.salary() > 3000).count();
int totalSalary = staff.stream().mapToInt(Employee::salary).sum();
System.out.println("Lowest salary: " + lowestPaid.map(Employee::salary).orElse(0));
System.out.println("Highest salary: " + highestPaid.map(Employee::salary).orElse(0));
System.out.println("Count salary > 3000: " + countHighSalary);
System.out.println("Total salary sum: " + totalSalary);
Mapping Operations
Mapping transforms elements from one form to another.
peek: Performs an action on each element, useful for debugging. Its an intermediate operation.map: Applies a function to each element, transforming it.flatMap: Maps each element to a stream and flattens the resulting streams into one.
// Using peek for side-effects
List<Employee> updatedStaff = staff.stream()
.peek(e -> System.out.println("Before: " + e))
.peek(e -> { /* hypothetical salary update */ })
.peek(e -> System.out.println("After: " + e))
.collect(Collectors.toList());
// Using map for transformation
List<String> upperCaseNames = staff.stream()
.map(Employee::name)
.map(String::toUpperCase)
.collect(Collectors.toList());
// Using flatMap to flatten nested structures
record Team(String name, List<Employee> members) {}
List<Team> teams = List.of(
new Team("Alpha", List.of(new Employee("E1", "Alice", 4500, "IT"))),
new Team("Beta", List.of(new Employee("E2", "Bob", 3200, "HR")))
);
List<Employee> allMembers = teams.stream()
.flatMap(team -> team.members().stream())
.collect(Collectors.toList());
Reduction with reduce
The reduce operation combines elements to produce a single value.
// Sum of salaries using method reference
Optional<Integer> salarySumOpt = staff.stream()
.map(Employee::salary)
.reduce(Integer::sum);
// Sum of salaries with identity and accumulator
Integer salarySum = staff.stream()
.reduce(0, (partialSum, emp) -> partialSum + emp.salary(), Integer::sum);
// Product of salaries (using Long for larger numbers)
long salaryProduct = staff.stream()
.mapToLong(Employee::salary)
.reduce(1, (a, b) -> a * b);
Collecting Results
collect is a versatile terminal operation that gathers stream elements into a container or summary.
To Collections
List<Integer> evenNumbers = List.of(1, 2, 3, 4, 5, 6).stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
Set<String> departments = staff.stream()
.map(Employee::department)
.collect(Collectors.toSet());
Map<String, Integer> nameToSalary = staff.stream()
.collect(Collectors.toMap(Employee::name, Employee::salary));
Statistical Summaries
long totalCount = staff.stream().count();
Double avgSalary = staff.stream().collect(Collectors.averagingDouble(Employee::salary));
Optional<Integer> maxSalary = staff.stream().map(Employee::salary).max(Integer::compare);
int totalSalarySum = staff.stream().mapToInt(Employee::salary).sum();
DoubleSummaryStatistics stats = staff.stream().collect(Collectors.summarizingDouble(Employee::salary));
Grouping and Partitioning
// Partition by salary condition
Map<Boolean, List<Employee>> partitioned = staff.stream()
.collect(Collectors.partitioningBy(e -> e.salary() > 4000));
// Group by department
Map<String, List<Employee>> byDept = staff.stream()
.collect(Collectors.groupingBy(Employee::department));
// Multi-level grouping: by department, then by salary range
Map<String, Map<String, List<Employee>>> nestedGroup = staff.stream().collect(
Collectors.groupingBy(Employee::department,
Collectors.groupingBy(e -> e.salary() > 4000 ? "High" : "Standard"))
);
Joining Strings
String employeeSummary = staff.stream()
.map(e -> e.name() + " in " + e.department())
.collect(Collectors.joining("; "));
Sorting
Streams can be sorted using natural order or a custom comparator.
// Sort by salary ascending
List<String> namesSortedBySalary = staff.stream()
.sorted(Comparator.comparingInt(Employee::salary))
.map(Employee::name)
.collect(Collectors.toList());
// Sort by salary descending, then by name
List<String> complexSort = staff.stream()
.sorted(Comparator.comparingInt(Employee::salary).reversed()
.thenComparing(Employee::name))
.map(Employee::name)
.collect(Collectors.toList());
Merging, Limiting, and Skipping
Streams support operations to combine, deduplicate, or subset data.
Stream<String> streamA = Stream.of("x", "y", "z");
Stream<String> streamB = Stream.of("a", "b", "x");
// Merge and remove duplicates
List<String> merged = Stream.concat(streamA, streamB).distinct().collect(Collectors.toList());
// Take the first 3 elements
List<Integer> firstThree = Stream.iterate(0, i -> i + 5).limit(3).collect(Collectors.toList());
// Skip the first 2 elements, then take 3
List<Integer> skipped = Stream.iterate(0, i -> i + 5).skip(2).limit(3).collect(Collectors.toList());