V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 14 of 20

Lambda & Stream API

lambdas, method references, Stream chains, Collectors, parallel streams

Why Lambdas and Streams

Use case: when processing collections / streaming data, the traditional for-loop + temp-variable style is long and error-prone. Lambda + Stream let you write by "stating intent": filter, map, group, aggregate are each a one-line operation, closer to a math formula than to steps.

// WhyStream.java
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class WhyStream {
    record User(String name, int age, String dept) {}

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Alice", 30, "Eng"),
            new User("Bob",   25, "Sales"),
            new User("Carol", 35, "Eng"),
            new User("Dave",  40, "Sales")
        );

        // imperative: find names in dept Eng with age >= 30
        List<String> imperative = new ArrayList<>();
        for (User u : users) {
            if (u.dept().equals("Eng") && u.age() >= 30) {
                imperative.add(u.name());
            }
        }
        System.out.println(imperative);

        // declarative: with Stream, at a glance
        List<String> declarative = users.stream()
            .filter(u -> u.dept().equals("Eng"))
            .filter(u -> u.age() >= 30)
            .map(User::name)
            .collect(Collectors.toList());
        System.out.println(declarative);
    }
}

Lambda Expression Syntax

A lambda is shorthand for a functional interface (single-abstract-method interface). Forms: (params) -> expression or (params) -> { statements; return value; }. Parameter types are usually omittable; the compiler infers from context.

// LambdaSyntax.java
import java.util.function.*;

public class LambdaSyntax {
    public static void main(String[] args) {
        // no args
        Supplier<String> greet = () -> "hello";
        System.out.println(greet.get());

        // single arg (parentheses optional)
        Function<Integer, Integer> square = x -> x * x;
        System.out.println(square.apply(5));

        // multiple args
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        System.out.println(add.apply(2, 3));

        // statement block
        Predicate<String> validEmail = s -> {
            if (s == null) return false;
            int at = s.indexOf('@');
            return at > 0 && at < s.length() - 1;
        };
        System.out.println(validEmail.test("a@b.c"));
    }
}

The Four Forms of Method Reference

When a lambda body just "directly calls an existing method", you can write a method reference — more concise.

// MethodRef.java
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class MethodRef {
    public static void main(String[] args) {
        // 1) static method of a class: Integer::parseInt is equivalent to s -> Integer.parseInt(s)
        Function<String, Integer> parse = Integer::parseInt;
        System.out.println(parse.apply("42"));

        // 2) instance method of an object: System.out::println
        List<String> names = List.of("a", "b", "c");
        names.forEach(System.out::println);

        // 3) instance method of a class: String::toUpperCase is equivalent to s -> s.toUpperCase()
        Function<String, String> upper = String::toUpperCase;
        System.out.println(upper.apply("hi"));

        // 4) constructor reference: ArrayList::new
        Supplier<java.util.ArrayList<String>> factory = java.util.ArrayList::new;
        System.out.println(factory.get());
    }
}

Creating Streams

A Stream is one-shot: it can't be reused after a terminal operation. Common ways to create: stream() from a collection / Arrays.stream / Stream.of / Stream.generate (infinite, needs limit) / IntStream.range (integer range).

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

public class StreamCreate {
    public static void main(String[] args) {
        // 1) from a collection
        List<Integer> list = List.of(1, 2, 3);
        list.stream().forEach(System.out::println);

        // 2) from an array
        int[] arr = {1, 2, 3};
        Arrays.stream(arr).forEach(System.out::println);

        // 3) Stream.of
        Stream.of("a", "b", "c").forEach(System.out::println);

        // 4) integer range (half-open)
        IntStream.range(0, 5).forEach(System.out::println);

        // 5) infinite stream + limit
        Stream.iterate(1, n -> n * 2).limit(5).forEach(System.out::println);
        // 1 2 4 8 16
    }
}

Intermediate Ops: filter / map / flatMap / sorted / distinct / limit

Intermediate operations return a new Stream and can be chained. They are lazy — they only run when a terminal operation is reached.

// StreamIntermediate.java
import java.util.List;
import java.util.stream.Collectors;

public class StreamIntermediate {
    public static void main(String[] args) {
        List<String> words = List.of("apple", "banana", "cherry", "avocado", "berry");

        // filter: keep elements matching the condition
        List<String> withA = words.stream()
            .filter(w -> w.startsWith("a"))
            .collect(Collectors.toList());
        System.out.println(withA);

        // map: transform each element
        List<Integer> lengths = words.stream()
            .map(String::length)
            .collect(Collectors.toList());
        System.out.println(lengths);

        // flatMap: turn each element into a Stream, then flatten
        List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
        List<Integer> flat = nested.stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());
        System.out.println(flat); // [1, 2, 3, 4]

        // sorted + distinct + limit combined
        List<Integer> top3 = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5).stream()
            .distinct()
            .sorted(java.util.Comparator.reverseOrder())
            .limit(3)
            .collect(Collectors.toList());
        System.out.println(top3); // [9, 6, 5]
    }
}

Terminal Ops: forEach / collect / reduce / count / anyMatch / findFirst

A terminal operation triggers computation and consumes the Stream (can't be reused afterward). It returns not a Stream but a concrete value/collection/Optional.

// StreamTerminal.java
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class StreamTerminal {
    public static void main(String[] args) {
        List<Integer> nums = List.of(1, 2, 3, 4, 5);

        // count
        long even = nums.stream().filter(n -> n % 2 == 0).count();
        System.out.println("even count = " + even);

        // anyMatch / allMatch / noneMatch
        System.out.println(nums.stream().anyMatch(n -> n > 3));   // true
        System.out.println(nums.stream().allMatch(n -> n > 0));   // true
        System.out.println(nums.stream().noneMatch(n -> n < 0));  // true

        // findFirst: returns Optional
        Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
        System.out.println(first.orElse(-1));

        // reduce
        int sum = nums.stream().reduce(0, Integer::sum);
        System.out.println("sum = " + sum);

        // collect to a List
        List<Integer> doubled = nums.stream()
            .map(n -> n * 2)
            .collect(Collectors.toList());
        System.out.println(doubled);
    }
}

Collectors: Grouping, Statistics, Joining

Collectors are "plugins" for Stream.collect, providing terminal operations like grouping (groupingBy), collecting into a Map by key (toMap), string joining (joining), and statistics (counting / summingInt / averagingDouble). **This is what you use most with Stream in practice.**

// CollectorsDemo.java
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectorsDemo {
    record Order(String user, String product, int amount) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Alice", "book",   30),
            new Order("Bob",   "book",   25),
            new Order("Alice", "pen",    10),
            new Order("Carol", "book",   60),
            new Order("Bob",   "pen",     5)
        );

        // 1) joining: all user names (deduped + comma)
        String names = orders.stream()
            .map(Order::user)
            .distinct()
            .collect(Collectors.joining(", "));
        System.out.println(names); // Alice, Bob, Carol

        // 2) group by user
        Map<String, List<Order>> byUser = orders.stream()
            .collect(Collectors.groupingBy(Order::user));
        System.out.println(byUser);

        // 3) aggregate total amount by user
        Map<String, Integer> totalByUser = orders.stream()
            .collect(Collectors.groupingBy(
                Order::user,
                Collectors.summingInt(Order::amount)
            ));
        System.out.println(totalByUser); // {Alice=40, Bob=30, Carol=60}

        // 4) count by product
        Map<String, Long> countByProduct = orders.stream()
            .collect(Collectors.groupingBy(
                Order::product,
                Collectors.counting()
            ));
        System.out.println(countByProduct);

        // 5) user name -> total amount (toMap)
        Map<String, Integer> totalMap = orders.stream()
            .collect(Collectors.toMap(
                Order::user,
                Order::amount,
                Integer::sum   // merge function when the same key appears multiple times
            ));
        System.out.println(totalMap);
    }
}

IntStream / LongStream / DoubleStream

Numeric-specialized streams that avoid box/unbox overhead. They provide numeric aggregations like sum / average / max / min / summaryStatistics.

// NumericStream.java
import java.util.IntSummaryStatistics;
import java.util.stream.IntStream;

public class NumericStream {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 100).sum();
        System.out.println("1..100 sum = " + sum); // 5050

        IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6).summaryStatistics();
        System.out.println(stats);
        // IntSummaryStatistics{count=8, sum=31, min=1, average=3.875000, max=9}

        // convert a plain Stream<Integer> to an IntStream
        int total = java.util.List.of(1, 2, 3).stream().mapToInt(Integer::intValue).sum();
        System.out.println(total);
    }
}

Parallel Streams: When to Use parallelStream()

Calling .parallelStream() or .parallel() lets data be processed by multiple cores at once. **Note: faster is not always better.** Use it only when the data is large (tens of thousands+), the operation is CPU-intensive, and stateless; with IO / order-sensitive / shared mutable state it's slower and more dangerous.

// Parallel.java
import java.util.stream.IntStream;

public class Parallel {
    public static void main(String[] args) {
        // compute the sum of squares of 1..1_000_000
        long sum = IntStream.rangeClosed(1, 1_000_000)
            .parallel()
            .mapToLong(n -> (long) n * n)
            .sum();
        System.out.println(sum);
    }
}

In Practice: Top-3 Users Aggregated from Orders

A common business scenario combining filter + groupingBy + summingInt + sorted + limit: find the top-3 spending users from order data.

// TopUsers.java
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TopUsers {
    record Order(String user, String product, int amount, String status) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Alice", "book",  30, "paid"),
            new Order("Bob",   "book",  25, "paid"),
            new Order("Alice", "pen",   10, "refund"),
            new Order("Carol", "book",  60, "paid"),
            new Order("Dave",  "pen",   90, "paid"),
            new Order("Bob",   "pen",    5, "paid")
        );

        // top-3 paid-user spend
        List<Map.Entry<String, Integer>> top3 = orders.stream()
            .filter(o -> o.status().equals("paid"))
            .collect(Collectors.groupingBy(Order::user, Collectors.summingInt(Order::amount)))
            .entrySet().stream()
            .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
            .limit(3)
            .collect(Collectors.toList());

        top3.forEach(e -> System.out.println(e.getKey() + " -> " + e.getValue()));
        // Dave -> 90, Carol -> 60, Bob -> 30
    }
}