Working with Java Streams for Data Processing

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());

Tags: java Stream API Functional Programming Data Processing Collections

Posted on Sun, 28 Jun 2026 17:29:04 +0000 by Yanayaya