Skip to content

Streams (course memo 1 & 2)

Java. Changing elements grouped in a collection tends to follow the same repeated shape of code. Streams are a template tool for that: you pick the steps to apply like building blocks, and describe how each element changes with a lambda expression.

Starting a Stream

java
List<String> things = List.of("table", "chair");
things.stream()
    // ...

Stream.of("table", "chair")
    // ...

Start a stream via .stream() on a collection (e.g. List), or via the Stream class directly (Stream.of(...)).

Ending a Stream (terminal operation)

java
List<Integer> colours = List.of(1, 2, 3, 4, 5);
Optional<Integer> oNumber = colours.stream()
        .filter(number -> number > 3)
        .findFirst();

List<String> vowels = Stream.of("a", "e", "i")
        .map(vowel -> vowel.toUpperCase())
        .collect(Collectors.toList());

The end of a stream is its terminal operation — every stream has exactly one. Starting with findFirst and collect.

Continuing a Stream (intermediate operations)

java
List<Integer> numbers = Stream.of(1, 3, 5)
        .map(number -> number * 3)
        .collect(Collectors.toList());

List<String> days = List.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday");
List<String> tDays = days.stream()
        .filter(day -> day.startsWith("T"))
        .collect(Collectors.toList());

Each step of a stream is an intermediate operation — you can chain as many as you want, and each one returns another stream. Starting with map and filter.

Combining several steps

java
List<String> names = List.of("Rodrigo", "Laura", "Rosa", "Seth", "Lisa");

List<String> result = names.stream()
        .filter(name -> name.endsWith("sa"))
        .map(name -> name.replaceAll("a", "e"))
        .map(name -> name.toUpperCase())
        .filter(name -> name.startsWith("R"))
        .collect(Collectors.toList());

A stream: starts with .stream()/Stream.of(...), has zero or more intermediate operations, ends with exactly one terminal operation.

Lambda expressions

Stream operations that need extra behavior receive it as a lambda — a quick way to write a method.

java
int applyFormula(int number) {
    return (number * number) - 10;
}

number -> (number * number) - 10 // same logic, as a lambda

A regular method has a name, return type, parameters, body. A lambda skips the name and return type (both inferred) and keeps only parameters + body.

Structure:

java
(param1, param2) -> {
    System.out.println(param1);
    System.out.println(param2);
}

(e1, e2) -> System.out.println(e1 + e2)

e1 -> System.out.println(e1)

() -> System.out.println("Hello!")
  • Parameters go in parentheses, comma-separated; you choose the names.
  • -> separates parameters from body.
  • Multi-line body: curly braces, each statement ends with ;.
  • Single-line body: braces and ; can be dropped.
  • Single parameter: parentheses can be dropped. Zero parameters: parentheses are required (()).

Method references

java
.map(vowel -> vowel.toUpperCase())
.map(String::toUpperCase) // equivalent, simplified

Use a method reference (ClassName::methodName, no parentheses/parameters) when a lambda's whole body is just calling one existing method with no extra logic.

More intermediate operations

  • skip(n) — ignore the first n elements.
  • limit(n) — stop after exactly n elements have passed through.
  • sorted() — sort by natural/default order.
  • sorted(Comparator) — sort by custom rules.
java
// Skip the first two
List<Integer> numbers = Stream.of(1, 2, 3, 4, 5).skip(2).collect(Collectors.toList());

// Keep only the first three
List<Integer> numbers = Stream.of(1, 2, 3, 4, 5).limit(3).collect(Collectors.toList());

// Sort, default order
List<Integer> numbers = Stream.of(4, 2, 1, 5, 3).sorted().collect(Collectors.toList());

// Sort, custom order
List<Integer> numbers = Stream.of(4, 2, 1, 5, 3)
        .sorted(Comparator.reverseOrder())
        .collect(Collectors.toList());

Comparators

java
.sorted(Comparator.naturalOrder())
.sorted(Comparator.reverseOrder())
.sorted(Comparator.comparing(Pet::getName))
.sorted(Comparator.comparing(Pet::getName).reversed())
.sorted(Comparator.comparing(Pet::getName).thenComparing(Pet::getOwner))
  • naturalOrder()/reverseOrder() — delegate to each element's own compareTo.
  • comparing(getter) — sort by one specific attribute (via a method reference).
  • .reversed() — flips a comparator.
  • .thenComparing(getter) — tiebreaker, applied when the first comparison is equal.
java
List<Pet> pets = Stream.of(
                new Pet("Mittens", "Tom"),
                new Pet("Cooper", "Lucy"))
        .sorted(Comparator.comparing(Pet::getName)
                .thenComparing(Pet::getOwner))
        .collect(Collectors.toList());

Collectors

java
List<Integer> numbers = Stream.of(1, 2, 3).collect(Collectors.toList());

String text = Stream.of("1", "2", "3").collect(Collectors.joining());       // "123"
String delimited = Stream.of("1", "2", "3").collect(Collectors.joining(", ")); // "1, 2, 3"
  • toList() — collect elements into a List.
  • joining() — concatenate strings with no delimiter.
  • joining(delimiter) — concatenate with a delimiter between elements (not at the start/end). Many other Collectors methods exist for different groupings.

More terminal operations

  • count() — number of elements reaching this point.
  • forEach(lambda) — perform a side effect per element.
  • reduce(lambda) — combine all elements into one via the given operation (e.g. sum); returns Optional since there might be no elements.
  • reduce(start, lambda) — same, but returns start if the stream is empty, and uses start as the seed value.
java
long count = Stream.of(5, 2, 3, 1, 4).count();

Stream.of(5, 2, 3, 1, 4).forEach(e -> System.out.println(e));

Optional<Integer> oSum = Stream.of(1, 2, 3).reduce(Integer::sum);

Integer sum = Stream.of(1, 2, 3).reduce(0, Integer::sum);