Functional Programming: Lambda and Streams
Introduction
Java 8+ added functional programming features: lambda expressions, method references, and the Stream API for declarative data processing. This chapter uses one dataset throughout—student names and scores—so you see how pieces connect. @FunctionalInterface was introduced in Annotations.
Prerequisites
- Interfaces, Generics
- Collections helps but not required for basic streams
1) Functional Interfaces
A functional interface has exactly one abstract method (SAM).
@FunctionalInterface
public interface ScoreFilter {
boolean test(int score);
}Implement with lambda:
ScoreFilter passing = score -> score >= 60;
System.out.println(passing.test(75)); // trueJDK built-ins in java.util.function:
Predicate<T>—boolean test(T t)Function<T,R>—R apply(T t)Consumer<T>—void accept(T t)Supplier<T>—T get()
2) Lambda Expressions
Shorthand for anonymous function objects.
// Before — anonymous class
ScoreFilter f1 = new ScoreFilter() {
@Override
public boolean test(int score) {
return score >= 90;
}
};
// Lambda
ScoreFilter f2 = score -> score >= 90;Syntax variants:
(a, b) -> a + b
x -> x * 2
() -> System.out.println("Hi")Type inference supplies parameter types when context is clear.
3) Method References
When lambda only calls an existing method:
List<String> names = List.of("emma", "liam");
names.forEach(System.out::println); // instance method ref
names.stream()
.map(String::toUpperCase) // static/instance ref form
.forEach(System.out::println);Forms: Class::staticMethod, instance::method, Class::new.
4) Stream API Overview
A Stream processes elements from a source (collection, array) in a pipeline:
source → intermediate ops → terminal op
Same data for examples:
import java.util.List;
record Student(String name, int score) {}
List<Student> students = List.of(
new Student("Emma", 95),
new Student("Liam", 78),
new Student("Noah", 92),
new Student("Ava", 85)
);filter — keep matching elements
List<Student> top = students.stream()
.filter(s -> s.score() >= 90)
.toList();
System.out.println(top); // [Emma]map — transform each element
List<String> names = students.stream()
.map(Student::name)
.map(String::toUpperCase)
.toList();sorted — order elements
List<Student> ranked = students.stream()
.sorted((a, b) -> Integer.compare(b.score(), a.score()))
.toList();collect — build result container
import java.util.stream.Collectors;
String summary = students.stream()
.filter(s -> s.score() >= 80)
.map(Student::name)
.collect(Collectors.joining(", "));
System.out.println(summary);Terminal operations (must end pipeline)
forEach, count, toList, collect, reduce, findFirst, etc.
Without a terminal op, the stream does nothing (lazy intermediates).
5) Full Pipeline Example
import java.util.List;
public class StreamDemo {
record Student(String name, int score) {}
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Emma", 95),
new Student("Liam", 78),
new Student("Noah", 92),
new Student("Ava", 85)
);
double avgPassing = students.stream()
.filter(s -> s.score() >= 60)
.mapToInt(Student::score)
.average()
.orElse(0.0);
System.out.printf("Average passing score: %.1f%n", avgPassing);
students.stream()
.sorted((a, b) -> Integer.compare(b.score(), a.score()))
.forEach(s -> System.out.println(s.name() + " -> " + s.score()));
}
}mapToInt avoids boxing for numeric aggregates.
6) Parallel Streams (Brief)
long count = students.parallelStream()
.filter(s -> s.score() > 80)
.count();Uses multiple threads—good for large CPU-bound data. Thread safety and ordering nuances belong in Multithreading. Start with sequential streams until you understand behavior.
Lambda vs Stream — When to Use
| Use | When |
|---|---|
| Lambda alone | callbacks, comparators, small handlers |
| Streams | transform/filter/aggregate collections declaratively |
| Classic loops | very simple logic, need break/continue on complex control flow |
Common Beginner Mistakes
Modifying Source During Stream
Do not add/remove from a list while streaming it—ConcurrentModificationException.
Missing Terminal Operation
students.stream().filter(s -> s.score() > 80); // does nothingReusing Stream
Streams are single-use—create a new stream for another pipeline.
Overusing Streams for Tiny Loops
Readability matters—a three-line for may beat a long stream chain.
Mini Practice
Given List<Integer> scores, print count of scores > 80 and their sum using streams.
What’s Next
Collections Overview — List, Set, Map with generics.
FAQ
Is lambda the same as anonymous inner class?
Similar role, different bytecode—lambdas use invokedynamic and are more concise.
Can lambdas access local variables?
Yes if effectively final (not reassigned after capture).
Do streams replace collections?
No. Streams process collections; you still store data in List/Set/Map.
Why toList() vs collect(Collectors.toList())?
toList() (Java 16+) returns unmodifiable list—simpler for many cases.
When to avoid parallel streams?
Small data, IO-bound work, or when order and thread safety matter—sequential is safer default.