Java 26-Day Course - Day 19: Stream API

Day 19: Stream API

The Stream API is a tool for processing collection data declaratively. Instead of for loops, you simply describe “what to do.” Like a factory conveyor belt, data flows through a pipeline being transformed, filtered, and aggregated.

Stream Basics

The basic flow from stream creation through intermediate operations to terminal operations.

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamBasic {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Basic pipeline: source -> intermediate ops -> terminal op
        int sumOfEven = numbers.stream()    // 1. Create stream
            .filter(n -> n % 2 == 0)        // 2. Intermediate: even only
            .mapToInt(Integer::intValue)     // 3. Intermediate: convert to int
            .sum();                          // 4. Terminal: sum
        System.out.println("Even sum: " + sumOfEven); // 30

        // Ways to create streams
        // 1. From collections
        List<String> names = List.of("Alice", "Bob", "Charlie");
        Stream<String> nameStream = names.stream();

        // 2. From arrays
        int[] arr = {1, 2, 3, 4, 5};
        IntStream intStream = Arrays.stream(arr);

        // 3. Stream.of()
        Stream<String> stream = Stream.of("A", "B", "C");

        // 4. Range generation
        IntStream range = IntStream.rangeClosed(1, 100); // 1~100

        // 5. Infinite stream
        Stream<Double> randoms = Stream.generate(Math::random).limit(5);
        randoms.forEach(r -> System.out.printf("%.4f ", r));
        System.out.println();

        // Streams can only be used once! (not reusable)
        Stream<Integer> oneTime = numbers.stream();
        oneTime.forEach(System.out::print);
        // oneTime.count(); // IllegalStateException!
    }
}

Intermediate Operations

Operations that transform and filter data. They are lazily evaluated.

import java.util.List;
import java.util.stream.Stream;

record Product(String name, String category, int price) {}

public class IntermediateOps {
    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Laptop", "Electronics", 1500000),
            new Product("Mouse", "Electronics", 35000),
            new Product("Keyboard", "Electronics", 89000),
            new Product("Desk", "Furniture", 250000),
            new Product("Chair", "Furniture", 350000),
            new Product("Monitor", "Electronics", 450000),
            new Product("Book", "Stationery", 15000),
            new Product("Pen", "Stationery", 3000)
        );

        // filter: pass only elements matching the condition
        System.out.println("=== 100,000 or more ===");
        products.stream()
            .filter(p -> p.price() >= 100000)
            .forEach(p -> System.out.println(p.name() + ": " + p.price()));

        // map: transform elements
        System.out.println("\n=== Product names ===");
        products.stream()
            .map(Product::name)
            .forEach(System.out::println);

        // distinct: remove duplicates
        System.out.println("\n=== Category types ===");
        products.stream()
            .map(Product::category)
            .distinct()
            .forEach(System.out::println);

        // sorted: sort
        System.out.println("\n=== Sorted by price ===");
        products.stream()
            .sorted((a, b) -> Integer.compare(a.price(), b.price()))
            .forEach(p -> System.out.printf("%-10s %,d won%n", p.name(), p.price()));

        // peek: for debugging (inspect values mid-pipeline)
        System.out.println("\n=== Pipeline trace ===");
        long count = products.stream()
            .filter(p -> p.price() > 50000)
            .peek(p -> System.out.println("Passed filter: " + p.name()))
            .map(Product::name)
            .count();
        System.out.println("Result count: " + count);

        // flatMap: flatten nested structures
        List<List<String>> nested = List.of(
            List.of("a", "b"),
            List.of("c", "d"),
            List.of("e", "f")
        );
        nested.stream()
            .flatMap(List::stream) // List<List<String>> -> Stream<String>
            .forEach(s -> System.out.print(s + " "));
        System.out.println();
    }
}

Terminal Operations

Operations that consume the stream to produce a result.

import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;

record Employee(String name, String department, int salary) {}

public class TerminalOps {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", "Development", 5000),
            new Employee("Bob", "Planning", 4500),
            new Employee("Charlie", "Development", 6000),
            new Employee("Diana", "Design", 4800),
            new Employee("Eve", "Development", 5500),
            new Employee("Frank", "Planning", 4200)
        );

        // count: number of elements
        long devCount = employees.stream()
            .filter(e -> e.department().equals("Development"))
            .count();
        System.out.println("Development team size: " + devCount);

        // sum, average, min, max
        int totalSalary = employees.stream()
            .mapToInt(Employee::salary)
            .sum();
        System.out.println("Total salary: " + totalSalary);

        OptionalInt maxSalary = employees.stream()
            .mapToInt(Employee::salary)
            .max();
        System.out.println("Max salary: " + maxSalary.orElse(0));

        double avgSalary = employees.stream()
            .mapToInt(Employee::salary)
            .average()
            .orElse(0);
        System.out.printf("Average salary: %.0f%n", avgSalary);

        // findFirst, findAny
        Optional<Employee> firstDev = employees.stream()
            .filter(e -> e.department().equals("Development"))
            .findFirst();
        firstDev.ifPresent(e -> System.out.println("First developer: " + e.name()));

        // anyMatch, allMatch, noneMatch
        boolean hasHighPaid = employees.stream()
            .anyMatch(e -> e.salary() > 5500);
        System.out.println("Anyone above 5500? " + hasHighPaid);

        boolean allPositive = employees.stream()
            .allMatch(e -> e.salary() > 0);
        System.out.println("All positive? " + allPositive);

        // reduce: cumulative operation
        int sumReduce = employees.stream()
            .map(Employee::salary)
            .reduce(0, Integer::sum);
        System.out.println("Reduce sum: " + sumReduce);
    }
}

Collecting with Collectors

Collect stream results into various collections or formats.

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

record Student(String name, int score, String grade) {}

public class CollectorsExample {
    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("Alice", 85, "B"),
            new Student("Bob", 92, "A"),
            new Student("Charlie", 78, "C"),
            new Student("Diana", 95, "A"),
            new Student("Eve", 88, "B"),
            new Student("Frank", 91, "A")
        );

        // toList, toSet
        List<String> nameList = students.stream()
            .map(Student::name)
            .collect(Collectors.toList());
        Set<String> gradeSet = students.stream()
            .map(Student::grade)
            .collect(Collectors.toSet());
        System.out.println("Names: " + nameList);
        System.out.println("Grade types: " + gradeSet);

        // joining: string concatenation
        String allNames = students.stream()
            .map(Student::name)
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("All: " + allNames);

        // groupingBy: grouping
        Map<String, List<Student>> byGrade = students.stream()
            .collect(Collectors.groupingBy(Student::grade));
        byGrade.forEach((grade, list) -> {
            System.out.println("Grade " + grade + ": " +
                list.stream().map(Student::name).collect(Collectors.joining(", ")));
        });

        // partitioningBy: split into two groups
        Map<Boolean, List<Student>> partition = students.stream()
            .collect(Collectors.partitioningBy(s -> s.score() >= 90));
        System.out.println("90+ : " +
            partition.get(true).stream().map(Student::name).toList());
        System.out.println("Below 90: " +
            partition.get(false).stream().map(Student::name).toList());

        // Statistics
        var stats = students.stream()
            .collect(Collectors.summarizingInt(Student::score));
        System.out.println("Average: " + stats.getAverage());
        System.out.println("Max: " + stats.getMax());
        System.out.println("Min: " + stats.getMin());
        System.out.println("Sum: " + stats.getSum());
        System.out.println("Count: " + stats.getCount());
    }
}

Today’s Exercises

  1. Sales Analysis: From a product list (name, category, price, quantity), use streams to calculate total sales per category (price x quantity), overall average price, and the most expensive product.

  2. String Processing Pipeline: Extract all words from a list of sentences (flatMap), convert to lowercase, filter only words with 3+ characters, remove duplicates, sort alphabetically, and collect into a final list.

  3. Employee Statistics Report: From an employee list, use streams to find the average salary per department, the highest-paid person per department, and the top 3 by salary overall, then print as a report.

Was this article helpful?