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
}
}