Lambdas & Streams
Lambdas
A lambda is an unnamed function. It fits anywhere a functional interface — an interface with
exactly one method — is expected.
Runnable task = () -> IO.println("running");
Predicate<String> isLong = s -> s.length() > 5;
BinaryOperator<Integer> add = (a, b) -> a + b;
Function<String, String> pretty = s -> {
var t = s.strip().toLowerCase();
return t.substring(0, 1).toUpperCase() + t.substring(1);
};
| Interface |
Shape |
Used by |
Predicate<T> |
T → boolean |
filter |
Function<T,R> |
T → R |
map |
Consumer<T> |
T → void |
forEach |
Supplier<T> |
() → T |
lazy values |
UnaryOperator<T> |
T → T |
replaceAll |
Comparator<T> |
(T,T) → int |
sorted |
Method references
s -> s.toUpperCase() → String::toUpperCase
s -> IO.println(s) → IO::println
s -> Integer.parseInt(s) → Integer::parseInt
() -> new ArrayList<>() → ArrayList::new
x -> obj.handle(x) → obj::handle
Streams
Source → any number of intermediate steps → exactly one terminal operation.
var result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
Intermediate steps are lazy: nothing happens without a terminal operation, and a stream is
single-use.
list.stream()
Arrays.stream(array)
Stream.of("a", "b")
IntStream.range(0, 10) IntStream.rangeClosed(1, 10)
Files.lines(path)
Stream.iterate(1, x -> x * 2).limit(10)
Stream.generate(Math::random).limit(5)
Intermediate: filter map flatMap distinct sorted limit skip peek
takeWhile dropWhile
Terminal: toList collect forEach count anyMatch allMatch noneMatch
findFirst min max reduce
Number streams
IntStream.rangeClosed(1, 100).sum(); // 5050
ages.stream().mapToInt(Integer::intValue).average().orElse(0);
var stats = ages.stream().mapToInt(Integer::intValue).summaryStatistics();
// min, max, sum, count, average
Collectors
import static java.util.stream.Collectors.*;
record Person(String name, int age, String city) {}
people.stream().collect(groupingBy(Person::city));
people.stream().collect(groupingBy(Person::city, counting()));
people.stream().collect(groupingBy(Person::city, mapping(Person::name, toList())));
people.stream().collect(partitioningBy(p -> p.age() >= 30));
people.stream().collect(toMap(Person::name, Person::age));
people.stream().map(Person::name).collect(joining(", ", "[", "]"));
people.stream().collect(averagingInt(Person::age));
Sorting:
people.stream()
.sorted(Comparator.comparingInt(Person::age)
.thenComparing(Person::name)
.reversed());
Optional
A container that may hold a value — it makes “might be missing” visible in the type.
Optional<Person> found = people.stream()
.filter(p -> p.name().equals("Ann"))
.findFirst();
found.isPresent()
found.orElse(fallback)
found.orElseGet(() -> loadDefault())
found.orElseThrow(() -> new IllegalStateException("not found"))
found.map(Person::name).orElse("unknown")
found.ifPresent(p -> IO.println(p.name()));
found.ifPresentOrElse(p -> …, () -> …);
Tip
Use Optional as a return type. Not as a field, parameter or collection element. And
get() without checking is as dangerous as an unchecked null.
Gatherers (Java 24+)
Custom intermediate operations:
import java.util.stream.Gatherers;
readings.stream().gather(Gatherers.windowSliding(3)).forEach(IO::println);
readings.stream().gather(Gatherers.windowFixed(2)).toList();
When not to use a stream
- a plain
for loop is shorter or clearer
- you need
break in the middle (though takeWhile often fits)
- your
forEach mutates outside state — that is a smell
parallelStream() only pays off for large, side-effect-free workloads: measure
★ Exercises
- From 20 numbers, keep the even ones, square them and sum — one chain.
- Sort words by length, then alphabetically.
- Group words by first letter into
Map<Character, List<String>>.
- Count word frequency into
Map<String, Long>.
- Write
Optional<Person> oldest(List<Person>). What does it return for an empty list?
- Generate the first 15 Fibonacci numbers with
Stream.iterate.
- Rewrite the word counter from chapter 7 using streams.