Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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
4

Every 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:

  1. A source — where the elements come from: a collection, an array, a set of values.
  2. Intermediate operations — filter, map, sorted and others. Each returns a new stream, so they can be chained.
  3. A terminal operation — forEach, count, toList and 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 3

Stream.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.

Thanks for reading!

Donate to the site — we work hard for you.