What a stream is
A stream is a chain of operations over a sequence of elements: "take a list, keep what you need, transform it, collect the result". A stream does not store data and does not change the source collection — it only describes what to do with the elements. Don't confuse it with threads from the concurrency chapter: they are different things with a similar name. That said, a stream can be processed by several threads at once — see the parallel streams lesson.
The first stream
Instead of a loop with a condition you can describe the whole task at once: keep the even numbers of a list and print them.
void main() {
List<Integer> numbers = List.of(5, 2, 8, 3, 4);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> IO.println(n));
}Output:
2
8
4Every collection has a stream() method. Then come the operations, each taking a lambda — a short function covered in the generics and lambdas chapter.
The three parts of a stream
Every stream has three parts:
- A source — where the elements come from: a collection, an array, a set of values.
- Intermediate operations —
filter,map,sortedand others. Each returns a new stream, so they can be chained. - A terminal operation —
forEach,count,toListand others. It starts the computation and produces the result.
void main() {
List<String> names = List.of("Anna", "Oleg", "Ira", "Andrew");
List<String> result = names.stream() // source
.filter(n -> n.startsWith("A")) // intermediate
.map(n -> n.toUpperCase()) // intermediate
.toList(); // terminal
IO.println(result);
}Output:
[ANNA, ANDREW]Where streams come from
A stream can come from more than a list:
void main() {
Stream<String> words = Stream.of("cat", "house", "garden");
IO.println(words.count());
int[] prices = {120, 80, 45};
IO.println(Arrays.stream(prices).sum());
IntStream.range(1, 4).forEach(i -> IO.println("Step " + i));
}Output:
3
245
Step 1
Step 2
Step 3Stream.of creates a stream from the listed values, Arrays.stream from an array. IntStream.range(1, 4) produces the numbers 1 to 3: the upper bound is excluded. IntStream is a special stream of int numbers with handy methods such as sum().
Lazy evaluation
Intermediate operations are lazy: until there is a terminal operation, they don't run. To see this, let's print something inside filter:
void main() {
Stream<Integer> stream = Stream.of(1, 2, 3)
.filter(n -> {
IO.println("Checking " + n);
return n > 1;
});
IO.println("Stream created");
IO.println(stream.toList());
}Output:
Stream created
Checking 1
Checking 2
Checking 3
[2, 3]The "Checking" lines appeared only after toList(). Thanks to laziness a stream does no extra work: for example, findFirst() stops at the first matching element.
Stream or loop
A stream doesn't replace loops completely. It shines when the task sounds like "select, transform, count". If you need to exit early on a complex condition or change several variables at once, a plain for is often clearer. Pick whatever reads more easily.
Common mistake
A stream can be traversed only once. After the terminal operation it is "used up", and a second call throws an exception:
void main() {
Stream<String> stream = Stream.of("a", "b");
IO.println(stream.count());
IO.println(stream.count());
}Output:
2
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at Main.main(Main.java:4)If you need the data again, keep it in a collection and create a fresh stream with stream() each time.