Streams

The Stream API lets you process collections declaratively: say what you want — filter these, transform those, add them up — instead of writing loops that say how. Stream pipelines are concise, readable, and chain naturally with lambdas.

A first pipeline

JAVA
import java.util.List;

record Product(String name, String category, double price) {}

public class Main {
    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Mug", "Home", 12.5),
            new Product("Earbuds", "Electronics", 59.99),
            new Product("Lamp", "Home", 34.0),
            new Product("Charger", "Electronics", 19.99),
            new Product("Throw", "Home", 49.0)
        );

        List<String> cheapHome = products.stream()
            .filter(p -> p.category().equals("Home"))
            .filter(p -> p.price() < 40)
            .map(Product::name)
            .sorted()
            .toList();

        System.out.println(cheapHome);
    }
}
Output
[Lamp, Mug]

A stream pipeline has three parts:

  1. A source: products.stream().
  2. Intermediate operations that return a new stream: filter, map, sorted…
  3. A terminal operation that produces the result: toList, count, forEach…

Streams don't modify the source collection. They're also lazy: nothing happens until the terminal operation runs.

Common intermediate operations

JAVA
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> nums = List.of(5, 3, 8, 3, 1, 9, 8);
        System.out.println(nums.stream().filter(n -> n > 3).toList());
        System.out.println(nums.stream().map(n -> n * 10).toList());
        System.out.println(nums.stream().distinct().sorted().toList());
        System.out.println(nums.stream().sorted((a, b) -> b - a).limit(3).toList());
        System.out.println(nums.stream().skip(5).toList());
        System.out.println(nums.stream().takeWhile(n -> n != 1).toList());
    }
}
Output
[5, 8, 9, 8]
[50, 30, 80, 30, 10, 90, 80]
[1, 3, 5, 8, 9]
[9, 8, 8]
[9, 8]
[5, 3, 8, 3]

Terminal operations

JAVA
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("stream", "lambda", "record", "switch", "yield");
        System.out.println(words.stream().count());
        System.out.println(words.stream().anyMatch(w -> w.startsWith("y")));
        System.out.println(words.stream().allMatch(w -> w.length() >= 5));
        System.out.println(words.stream().noneMatch(String::isEmpty));
        System.out.println(words.stream().filter(w -> w.contains("c")).findFirst().orElse("none"));
        System.out.println(words.stream().reduce("", (acc, w) -> acc + w.charAt(0)));
        words.stream().filter(w -> w.length() == 5).forEach(System.out::println);
    }
}
Output
5
true
true
true
record
slrsy
yield

findFirst returns an Optional (lesson 48), because the stream might be empty. reduce combines all elements into one value.

Numeric streams

JAVA
import java.util.List;
import java.util.stream.IntStream;

record Product(String name, double price, int stock) {}

public class Main {
    public static void main(String[] args) {
        List<Product> products = List.of(new Product("Mug", 12.5, 200), new Product("Lamp", 34.0, 15), new Product("Throw", 49.0, 30));

        System.out.println(products.stream().mapToInt(Product::stock).sum());
        System.out.println(products.stream().mapToDouble(Product::price).average().orElse(0));
        System.out.println(products.stream().mapToDouble(Product::price).max().getAsDouble());
        System.out.println(IntStream.rangeClosed(1, 5).map(n -> n * n).sum());
        System.out.println(IntStream.range(0, 5).boxed().toList());
        var stats = products.stream().mapToDouble(Product::price).summaryStatistics();
        System.out.println(stats.getMin() + " " + stats.getMax() + " " + stats.getCount());
    }
}
Output
245
31.833333333333332
49.0
55
[0, 1, 2, 3, 4]
12.5 49.0 3

IntStream, LongStream and DoubleStream work with primitives and add sum, average, max and summaryStatistics. IntStream.range replaces many counting loops.

flatMap

JAVA
import java.util.List;

record Order(int id, List<String> items) {}

public class Main {
    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order(1, List.of("mug", "lamp")),
            new Order(2, List.of("mug")),
            new Order(3, List.of("throw", "mug", "charger"))
        );
        List<String> allItems = orders.stream()
            .flatMap(o -> o.items().stream())
            .distinct()
            .toList();
        System.out.println(allItems);
    }
}
Output
[mug, lamp, throw, charger]

flatMap turns each element into a stream and concatenates them — here, from a stream of orders to one stream of all items.

Creating streams

JAVA
import java.util.Arrays;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        System.out.println(Stream.of("a", "b", "c").toList());
        System.out.println(Arrays.stream(new int[]{4, 5, 6}).sum());
        System.out.println(Stream.iterate(1, n -> n * 2).limit(8).toList());
        System.out.println("hello".chars().filter(c -> c == 'l').count());
    }
}
Output
[a, b, c]
15
[1, 2, 4, 8, 16, 32, 64, 128]
2

Streams or loops?

  • Streams shine for transforming and summarising collections: filter, map, group, total.
  • Loops are clearer when you need to break early in complex ways, modify several variables, or handle checked exceptions.
  • Keep lambdas in pipelines short and free of side effects; extract longer logic into named methods.
  • A stream can be used only once.

Try it yourself

From a list of words, get the distinct lengths of words longer than three letters, in descending order.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
[13, 6, 4]