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
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); } }
[Lamp, Mug]
A stream pipeline has three parts:
- A source:
products.stream(). - Intermediate operations that return a new stream:
filter,map,sorted… - 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
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()); } }
[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
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); } }
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
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()); } }
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
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); } }
[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
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()); } }
[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.
[13, 6, 4]