Java Lambdas, Streams, and Internal Iteration

Part 4 of 9 in Functional Java Unleashed

Java 8 introduced two features that reshaped how Java code is written: lambda expressions and the Stream API. Before them, callback-style code looked like a syntax tax — an Runnable needed a full class body just to return 1 + 2. Lambdas collapsed that boilerplate to () -> 1 + 2. Combined with the Stream API’s fluent chain of intermediate operations terminated by collectors, it became possible to describe what you want from data rather than how to iterate over it.

This post walks through all three layers: lambda syntax replacing anonymous classes, the standard functional interfaces that make lambdas composable, and a realistic stream pipeline using Collectors for grouping, partitioning, summarizing, and aggregating structured data.

The code

The single file demonstrates three distinct patterns — each section is self-contained so you can skip to whichever piece matters.

  • Anonymous class vs. lambda shows the before/after comparison with a minimal functional interface.
  • Functional interfaces exercises Predicate, Function, Consumer, and BiFunction to show how different return types map to different interface shapes.
  • Stream pipeline works through a realistic scenario: filter, sort, group by, partition by threshold, summarize numeric stats, and join — all from the same List<Employee> source without any explicit loops.
import java.util.*;
import java.util.stream.*;
import java.util.function.*;

public class Demo {

    // --- 1. Lambda syntax replacing anonymous classes ---

    interface Task { String execute(); }

    static void anonymousClassExample() {
        System.out.println("=== Anonymous Class (Java 7 style) ===");
        Task oldWay = new Task() {
            @Override
            public String execute() {
                return "verbose anonymous class";
            }
        };
        System.out.println("Old way: " + oldWay.execute());

        System.out.println("\n=== Lambda (Java 8 style) ===");
        Task lambdaWay = () -> "concise lambda";
        System.out.println("New way: " + lambdaWay.execute());
    }

    // --- 2. Standard functional interfaces ---

    static void functionalInterfacesExample() {
        System.out.println("\n=== Functional Interfaces ===");

        // Predicate<T> — T -> boolean
        Predicate<Integer> isEven = n -> n % 2 == 0;
        System.out.println("isEven.test(4): " + isEven.test(4));   // true
        System.out.println("isEven.test(7): " + isEven.test(7));   // false

        // Function<T, R> — T -> R
        Function<String, Integer> lengthFn = s -> s.length();
        System.out.println("lengthFn.apply('hello'): " + lengthFn.apply("hello"));  // 5
        System.out.println("lengthFn.apply('hi'): " + lengthFn.apply("hi"));       // 2

        // Consumer<T> — T -> void
        Consumer<String> printer = s -> System.out.println("  consumed: " + s);
        printer.accept("world");

        // BiFunction for two arguments
        BiFunction<Double, Double, Double> distance =
            (x1, y1) -> Math.hypot(x1, y1);
        double d = distance.apply(3.0, 4.0);
        System.out.println("hypot(3, 4): " + d);  // 5.0
    }

    // --- 3. Stream pipeline with Collectors ---

    static record Employee(String name, int salary, String dept) {}

    static void streamExample() {
        List<Employee> staff = List.of(
            new Employee("Alice",   95000, "Engineering"),
            new Employee("Bob",     85000, "Engineering"),
            new Employee("Carol",   110000,"Marketing"),
            new Employee("Dave",    92000, "Engineering"),
            new Employee("Eve",     78000, "Marketing"),
            new Employee("Frank",   105000,"Sales")
        );

        System.out.println("\n=== Stream Operations on Employee Data ===");

        // Filter + sorted by salary descending
        List<String> topEngineering = staff.stream()
            .filter(e -> e.dept().equals("Engineering"))
            .sorted(Comparator.comparingInt(Employee::salary).reversed())
            .map(Employee::name)
            .toList();
        System.out.println("Top earners in Engineering: " + topEngineering);

        // Grouping by department → map of List<Employee>
        Map<String, List<Employee>> byDept = staff.stream()
            .collect(Collectors.groupingBy(Employee::dept));
        System.out.println("\nStaff by department:");
        byDept.forEach((d, members) ->
            System.out.println("  " + d + ": " + members.size() + " people"));

        // Partitioning into above/below a threshold
        double avgSalary = staff.stream()
            .mapToInt(Employee::salary).average().orElse(0.0);
        System.out.println("\nAverage salary: $" + String.format("%.0f", avgSalary));

        Map<Boolean, List<Employee>> partitioned = staff.stream()
            .collect(Collectors.partitioningBy(
                e -> e.salary() >= avgSalary));
        System.out.println("Above-average: " + partitioned.get(true).size() + " employees");
        System.out.println("Below-average:  " + partitioned.get(false).size() + " employees");

        // Collecting to a custom map (name → salary)
        Map<String, Integer> nameToSalary = staff.stream()
            .collect(Collectors.toMap(Employee::name, Employee::salary));
        System.out.println("\nName→Salary: " + nameToSalary);

        // Summarizing with summarizingInt collector
        IntSummaryStatistics stats = staff.stream()
            .collect(Collectors.summarizingInt(Employee::salary));
        System.out.println("\nSalary statistics:");
        System.out.println("  count:     " + stats.getCount());
        System.out.println("  min:       $" + stats.getMin());
        System.out.println("  max:       $" + stats.getMax());
        System.out.println("  average:   $" + String.format("%.0f", stats.getAverage()));
        System.out.println("  total:     $" + stats.getSum());

        // Joining names with Collectors.joining
        String allNames = staff.stream()
            .map(Employee::name)
            .collect(Collectors.joining(", "));
        System.out.println("\nAll names: " + allNames);
    }

    public static void main(String[] args) {
        anonymousClassExample();
        functionalInterfacesExample();
        streamExample();
    }
}

Running it

The program is compiled and run with javac then java — no framework dependencies required.

Here is what the recording of this code looks like:

A few observations from the output:

Lambda vs. anonymous class. The comparison shows that a lambda with zero parameters uses empty parens () ->, a single-parameter lambda drops the parens, and multi-parameter lambdas wrap their bodies in braces when needed. The functional interface (Task here) is inferred — you don’t name it at the assignment site anymore.

Functional interfaces are composable. Predicate<Integer> gives you boolean output (useful for filtering), Function<String, Integer> transforms between types (useful for extracting or projecting values), and Consumer<String> performs side effects without returning anything. Each interface has exactly one abstract method — the lambda body is that method’s implementation. BiFunction extends this pattern to two arguments.

Stream collectors eliminate imperative boilerplate. In the employee example, grouping by department used to require building a Map<String, List<Employee>> with nested loops and manual put-or-append logic. With Collectors.groupingBy, it’s one method call. Similarly, partitioning (the boolean variant of grouping) split the list into “above-average” and “below-average” in a single pipeline step.

The summarizingInt collector is particularly useful: one call produces count, min, max, average, and sum without manual tracking variables or multiple passes through the data.

Takeaway

Lambdas let you pass behavior as data without anonymous class boilerplate; functional interfaces define the shape of that behavior; and streams with collectors turn multi-step data transforms into a single declarative pipeline — filter and map for selection/transformation, collect for the final aggregation. The mental model is: the stream describes the computation; each intermediate operation adds a stage; the collector materializes the result.