Learn › Java Programming

Streams & Lambda

Master lambda expressions, functional interfaces, the Stream API, and collectors for modern functional-style Java programming.

Lambda Expressions

Lambda expressions, introduced in Java 8, provide a concise way to represent anonymous functions that implement a single abstract method. They use the arrow syntax with parameters on the left and the body on the right, eliminating the verbosity of anonymous inner classes. Lambda expressions can capture variables from the enclosing scope, provided those variables are effectively final. They are the foundation of functional-style programming in Java and are used extensively with the Stream API and collections.

import java.util.*;

public class LambdaDemo {
    public static void main(String[] args) {
        // Before lambda: anonymous inner class
        Comparator<String> oldWay = new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        };

        // With lambda expression
        Comparator<String> byLength = (a, b) -> a.length() - b.length();

        List<String> names = new ArrayList<>(
            List.of("Charlie", "Alice", "Bob", "Dave")
        );
        names.sort(byLength);
        System.out.println(names);  // [Bob, Dave, Alice, Charlie]

        // Method reference (shorthand for lambda)
        names.sort(Comparator.comparingInt(String::length));

        // Lambda with forEach
        names.forEach(name -> System.out.println("Hello, " + name));

        // Lambda stored in a variable
        Runnable task = () -> System.out.println("Task running!");
        task.run();
    }
}

Functional Interfaces

A functional interface is an interface with exactly one abstract method, making it eligible as the target type for lambda expressions. Java provides many built-in functional interfaces in the java.util.function package including Predicate, Function, Consumer, and Supplier. The @FunctionalInterface annotation is optional but recommended as it causes a compile error if the interface has more than one abstract method. Understanding these core interfaces allows you to compose powerful data transformations with minimal code.

import java.util.function.*;
import java.util.List;

public class FunctionalDemo {
    public static void main(String[] args) {
        // Predicate: takes T, returns boolean
        Predicate<String> isLong = s -> s.length() > 5;
        System.out.println(isLong.test("Hello"));        // false
        System.out.println(isLong.test("Hello World"));  // true

        // Function: takes T, returns R
        Function<String, Integer> toLength = String::length;
        System.out.println(toLength.apply("Java"));  // 4

        // Composing functions
        Function<String, String> trim = String::trim;
        Function<String, String> upper = String::toUpperCase;
        Function<String, String> pipeline = trim.andThen(upper);
        System.out.println(pipeline.apply("  hello  "));  // "HELLO"

        // Consumer: takes T, returns void
        Consumer<String> printer = System.out::println;
        List.of("A", "B", "C").forEach(printer);

        // Supplier: takes nothing, returns T
        Supplier<Double> random = Math::random;
        System.out.println(random.get());

        // BiFunction: takes T and U, returns R
        BiFunction<Integer, Integer, Integer> max = Math::max;
        System.out.println(max.apply(10, 20));  // 20
    }
}

The Stream API

The Stream API provides a declarative approach to processing collections of data through a pipeline of operations. Streams are lazy, meaning intermediate operations like filter, map, and sorted are not executed until a terminal operation triggers the computation. This lazy evaluation enables optimizations such as short-circuiting and loop fusion. Streams do not modify the underlying collection and can only be consumed once, promoting a functional programming style that avoids side effects.

import java.util.*;
import java.util.stream.*;

public class StreamDemo {
    public static void main(String[] args) {
        List<String> names = List.of(
            "Alice", "Bob", "Charlie", "David", "Eve", "Frank"
        );

        // Filter, map, and collect
        List<String> result = names.stream()
            .filter(name -> name.length() > 3)
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.toList());
        System.out.println(result);  // [ALICE, CHARLIE, DAVID, FRANK]

        // Chaining operations
        long count = names.stream()
            .filter(n -> n.startsWith("A") || n.startsWith("C"))
            .count();
        System.out.println("Count: " + count);  // 2

        // Finding elements
        Optional<String> first = names.stream()
            .filter(n -> n.length() == 3)
            .findFirst();
        first.ifPresent(n -> System.out.println("Found: " + n));  // "Found: Bob"

        // Numeric streams
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int sum = IntStream.of(numbers)
            .filter(n -> n % 2 == 0)
            .sum();
        System.out.println("Sum of evens: " + sum);  // 30

        // Generate and iterate
        List<Integer> squares = IntStream.rangeClosed(1, 5)
            .map(n -> n * n)
            .boxed()
            .collect(Collectors.toList());
        System.out.println(squares);  // [1, 4, 9, 16, 25]
    }
}

Collectors and Reduction

Collectors are terminal operations that accumulate stream elements into a result container such as a list, set, map, or string. The Collectors utility class provides factory methods for the most common collection strategies including grouping, partitioning, and summarizing. The reduce operation combines all elements into a single result using an associative accumulator function. These tools enable powerful data aggregation and transformation with concise, readable code.

import java.util.*;
import java.util.stream.*;

public class CollectorDemo {
    record Person(String name, String department, double salary) {}

    public static void main(String[] args) {
        List<Person> employees = List.of(
            new Person("Alice", "Engineering", 95000),
            new Person("Bob", "Marketing", 72000),
            new Person("Charlie", "Engineering", 105000),
            new Person("Diana", "Marketing", 68000),
            new Person("Eve", "Engineering", 88000)
        );

        // Joining strings
        String names = employees.stream()
            .map(Person::name)
            .collect(Collectors.joining(", "));
        System.out.println(names);  // Alice, Bob, Charlie, Diana, Eve

        // Grouping by department
        Map<String, List<Person>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Person::department));
        byDept.forEach((dept, people) ->
            System.out.println(dept + ": " + people.size()));

        // Average salary by department
        Map<String, Double> avgSalary = employees.stream()
            .collect(Collectors.groupingBy(
                Person::department,
                Collectors.averagingDouble(Person::salary)
            ));
        System.out.println(avgSalary);
        // {Engineering=96000.0, Marketing=70000.0}

        // Partitioning (boolean grouping)
        Map<Boolean, List<Person>> highEarners = employees.stream()
            .collect(Collectors.partitioningBy(p -> p.salary() > 80000));

        // Reduce to find total salary
        double totalSalary = employees.stream()
            .map(Person::salary)
            .reduce(0.0, Double::sum);
        System.out.println("Total payroll: 
quot; + totalSalary); } }

← Interfaces & Collections