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

  1. From 20 numbers, keep the even ones, square them and sum — one chain.
  2. Sort words by length, then alphabetically.
  3. Group words by first letter into Map<Character, List<String>>.
  4. Count word frequency into Map<String, Long>.
  5. Write Optional<Person> oldest(List<Person>). What does it return for an empty list?
  6. Generate the first 15 Fibonacci numbers with Stream.iterate.
  7. Rewrite the word counter from chapter 7 using streams.