Learn Java

Lesson 53Phase 6: Functional and modern Java

Streams: processing collections step by step

Describing what to do with a collection instead of how: filter, map, reduce and friends, and why streams are lazy.

By Updated 55 min

  • Stream
  • filter
  • map
  • reduce
  • forEach
  • collect

Five students have just taken a test, and you want the names of the ones who passed. You already know how to write that as a loop: make an empty list, walk the students, check each score, add each passing name. A stream lets you write the same job as a list of steps: take the students, keep the ones who passed, turn each one into a name, collect the names. You say what should happen to the elements, and Java does the walking. This lesson builds those steps one at a time, and then shows the order in which Java runs them, which is the part most people guess wrong at first.

By the end of this lesson
  • Build a pipeline from a source, intermediate operations such as filter and map, and one terminal operation such as toList, forEach or count.
  • Start a stream from a collection, from a few values with Stream.of, from an array with Arrays.stream, and from a range of numbers with IntStream.range and rangeClosed.
  • Explain laziness: nothing runs before the terminal operation, each element goes through every step before the next one starts, and limit and findFirst stop the work early.
  • Combine elements into one value with reduce, and use mapToInt to get the sum, average and max of an IntStream.
  • Write the four kinds of method reference, such as String::toUpperCase and System.out::println, in place of lambdas.
  • Say why a stream can be used only once, and choose between a stream and a plain loop.
From before
Streams run on lambdas. In enums and lambdas you learned that a lambda such as s -> s.score() >= 50is the body of a functional interface's one method, written short, and you met the method reference Math::max. Comparable, Comparator and sorting added Song::artist. The lists come from the Collections Framework, which also gave you a first look at a stream: orders.stream().filter(order -> order.equals("tea")).count(). The students on this page are records with a name and a score, and the difference between int and Integer from wrapper classes comes back later on this page.

From a loop to a stream#

Each student is a record with a name and a score, and a score of 50 or more is a pass. Here is the job written as a loop.

PassedLoop.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<Student> students = List.of(
7 new Student("Ada", 91),
8 new Student("Ben", 42),
9 new Student("Cleo", 67),
10 new Student("Dan", 38),
11 new Student("Eve", 82));
12
13 List<String> passed = new ArrayList<>();
14 for (Student s : students) {
15 if (s.score() >= 50) {
16 passed.add(s.name());
17 }
18 }
19 System.out.println(passed);
20 }
21}
22
23record Student(String name, int score) {}
Prints
›[Ada, Cleo, Eve]

It prints [Ada, Cleo, Eve]. The loop works, but most of its lines are about the mechanics: an empty list to fill, a loop variable, an if, a call to add. You have to read all of them to find the goal.

Here is the same job as a stream. Press Run, then step through it.

PassedStream.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42),
8 new Student("Cleo", 67),
9 new Student("Dan", 38),
10 new Student("Eve", 82));
11
12 List<String> passed = students.stream()
13 .filter(s -> s.score() >= 50)
14 .map(s -> s.name())
15 .toList();
16
17 System.out.println(passed);
18 }
19}
20
21record Student(String name, int score) {}
Prints
›[Ada, Cleo, Eve]

It prints [Ada, Cleo, Eve] as well. Read the four lines from students.stream() down as instructions: take the students, keep each one whose score is at least 50, turn each one into its name, and put the results in a list. There is no empty list to set up and no loop to write.

A stream is a sequence of elements that you process with a chain of steps. students.stream() gives you a stream of the students in the list. Each step is a method that hands a new stream to the next step, which is why the calls hang off each other with dots, one step per line.

While you step through, read the narration. It names every lambda call and the student it was given. Watch the order of those calls: it is the subject of the laziness section further down.

Coming from Python or JavaScript?
In Python you might write [s.name for s in students if s.score >= 50], and in JavaScript students.filter(s => s.score >= 50).map(s => s.name). Both run straight away, and the JavaScript version builds one new array for filter and another for map. A Java stream behaves more like a Python generator expression, which hands its elements along one at a time and does nothing until something asks for them.

Source, steps and a result#

Every stream program has the same three parts, and together they make a pipeline:

  • A source supplies the elements. Above, it is students.stream().
  • Zero or more intermediate operations, such as filter and map. Each one adds a step and gives back a new stream, so you can chain as many as you need.
  • Exactly one terminal operation at the end, such as toList. It produces the result, which is no longer a stream, and it finishes the pipeline.
LinePartWhat it does
students.stream()sourceHands the students over, one at a time.
.filter(s -> s.score() >= 50)intermediateLets a student through only when the lambda answers true.
.map(s -> s.name())intermediateTurns each student that arrives into a name.
.toList()terminalGathers whatever arrives into a new list and returns it.
The pipeline from PassedStream.java, line by line.

filter keeps, map changes

filter takes a lambda that answers true or false about one element. An element that gets true carries on, unchanged. An element that gets false is dropped. s -> s.score() >= 50 answers true for Ada, Cleo and Eve.

map takes a lambda that turns one element into a new value, and passes the new value on in its place. The new value can have a different type: s -> s.name() turns a Student into a String, so after map the stream carries strings. Here is the type of the stream after each step.

The stream's type after each step
read along
1List<String> passed = students.stream() // Stream<Student>
2 .filter(s -> s.score() >= 50) // still Stream<Student>
3 .map(s -> s.name()) // now Stream<String>
4 .toList(); // List<String>

The type in the angle brackets is what the next step receives. A lambda after map gets a name, so calling s.score() there would not compile: a String has no score() method.

The interfaces behind the lambdas

A lambda only means something once Java knows which functional interface it fills in. Each stream method declares one, taken from the package java.util.function, so you never have to write these interfaces yourself.

MethodTakes aWhose one method isA lambda for it
filterPredicate<T>boolean test(T)s -> s.score() >= 50
mapFunction<T, R>R apply(T)s -> s.name()
forEachConsumer<T>void accept(T)name -> System.out.println(name)
reduceBinaryOperator<T>T apply(T, T)(a, b) -> a + b
mapToIntToIntFunction<T>int applyAsInt(T)s -> s.score()
T is the type of the elements, and R is the type that map turns them into. forEach, reduce and mapToInt come later on this page.

Terminal operations: toList, forEach and count

The terminal operation decides what you get back. Three are worth knowing first:

  • toList() gathers the elements that reach it into a new List, in order.
  • forEach(action) runs the action on each element that reaches it, and returns nothing. It plays the part of a for-each loop's body.
  • count() returns how many elements reach it, as a long.
ForEachCount.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42),
8 new Student("Cleo", 67),
9 new Student("Dan", 38),
10 new Student("Eve", 82));
11
12 students.stream()
13 .filter(s -> s.score() >= 50)
14 .forEach(s -> System.out.println(s.name() + " passed with " + s.score()));
15
16 long notPassed = students.stream()
17 .filter(s -> s.score() < 50)
18 .count();
19 System.out.println(notPassed + " did not pass");
20 }
21}
22
23record Student(String name, int score) {}
Prints
›Ada passed with 91
›Cleo passed with 67
›Eve passed with 82
›2 did not pass

It prints Ada passed with 91, Cleo passed with 67 and Eve passed with 82, then 2 did not pass. Notice that the second pipeline starts again from students.stream(). A terminal operation uses its stream up, so a second job needs a second stream; the section on using a stream once shows what happens otherwise.

toList() and collect(Collectors.toList())

toList() arrived in Java 16. A lot of code, old and new, ends a pipeline with collect(Collectors.toList()) instead. collect is a terminal operation that takes a collector, a recipe for gathering elements into a result, and Collectors.toList() is the recipe for a list. Both give you the same elements. The difference shows up when you try to change the list.

ToList.java
1import java.util.List;
2import java.util.stream.Collectors;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> cart = List.of("pen", "notebook", "ink", "stapler");
7
8 List<String> viaCollect = cart.stream()
9 .filter(item -> item.length() > 3)
10 .collect(Collectors.toList());
11 viaCollect.add("glue");
12 System.out.println(viaCollect);
13
14 List<String> viaToList = cart.stream()
15 .filter(item -> item.length() > 3)
16 .toList();
17 System.out.println(viaToList);
18 viaToList.add("glue");
19 }
20}
Prints
›[notebook, stapler, glue]
›[notebook, stapler]
!Exception in thread "main" java.lang.UnsupportedOperationException
! at Main.main(Main.java:18)
Stops on line 18

UnsupportedOperationException

This collection cannot be changed. List.of and similar methods make read-only lists.

The first list prints [notebook, stapler, glue], because adding to it worked. The second prints [notebook, stapler], and then add throws an UnsupportedOperationException. The list from toList() cannot be changed, just like a list made with List.of.

Which one to write
Use toList() when you only read the result, which is most of the time. The documentation for Collectors.toList() makes no promise about the kind of list it returns. Today it is an ArrayList you can change, but if your code needs to add to the list, say so in the code: new ArrayList<>(stream.toList()) copies the result into a list that you know you can change.
Many more collectors
The class Collectors has recipes for much more than lists: grouping elements by a key, counting each group, splitting the elements into those that pass a test and those that fail it, and joining strings with commas. They are the subject of the next lesson, Collectors and stream recipes.

The source stays as it was

A stream never changes the collection it came from. filter leaves elements out of the stream; it does not remove them from the list. removeIf, from the Collections Framework, is the method that does.

SourceStays.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> fruit = new ArrayList<>(List.of("apple", "kiwi", "banana", "fig"));
7
8 List<String> shortNames = fruit.stream()
9 .filter(f -> f.length() <= 4)
10 .toList();
11 System.out.println(shortNames);
12 System.out.println(fruit);
13
14 fruit.removeIf(f -> f.length() <= 4);
15 System.out.println(fruit);
16 }
17}
Prints
›[kiwi, fig]
›[apple, kiwi, banana, fig]
›[apple, banana]

It prints [kiwi, fig], then the untouched [apple, kiwi, banana, fig]. Only removeIf changes the list itself, which leaves [apple, banana].

Key idea

A pipeline is a source, any number of intermediate operations, and one terminal operation. Each intermediate operation adds a step and gives back a stream; the terminal operation produces the result.

Quick check
In songs.stream().filter(s -> s.length() > 4).map(s -> s.toUpperCase()).count(), which call is the terminal operation?
ExerciseLongSongs.java
Finish the pipeline so that it prints the titles of the songs longer than 180 seconds: [Night Drive, Echo]. Change the two lambdas; leave the rest as it is.

Where streams come from#

stream() works on every collection: lists, sets, queues, and the keySet(), values() and entrySet() of a map. When your elements are not in a collection, there are other ways to start a stream.

SourceGives youExample
collection.stream()a stream of the collection's elements, in its orderstudents.stream()
Stream.of(a, b, c)a stream of the values you listStream.of("red", "green", "blue")
Arrays.stream(array)a stream of the array's elements, and an IntStream for an int[]Arrays.stream(temps)
IntStream.range(a, b)the whole numbers from a up to b, leaving b outIntStream.range(1, 5) gives 1, 2, 3, 4
IntStream.rangeClosed(a, b)the whole numbers from a to b, with b includedIntStream.rangeClosed(1, 5) gives 1 to 5
Stream lives in java.util.stream, and so does IntStream. Arrays lives in java.util.
Sources.java
1import java.util.Arrays;
2import java.util.List;
3import java.util.stream.IntStream;
4import java.util.stream.Stream;
5
6public class Main {
7 public static void main(String[] args) {
8 List<String> playlist = List.of("Intro", "Echo", "Outro");
9 System.out.println(playlist.stream().map(t -> t.toLowerCase()).toList());
10
11 Stream<String> colors = Stream.of("red", "green", "blue");
12 System.out.println(colors.filter(c -> c.length() > 3).toList());
13
14 String[] days = {"Mon", "Tue", "Wed", "Thu"};
15 System.out.println(Arrays.stream(days).count());
16
17 int[] temps = {18, 21, 19, 24};
18 System.out.println(Arrays.stream(temps).sum());
19
20 System.out.println(IntStream.range(1, 5).sum());
21 System.out.println(IntStream.rangeClosed(1, 5).sum());
22 }
23}
Prints
›[intro, echo, outro]
›[green, blue]
›4
›82
›10
›15

The six lines print [intro, echo, outro], [green, blue], 4, 82, 10 and 15. Arrays.stream on an int[] gives an IntStream, and so do the two range methods. An IntStream is a stream of plain int values with number methods such as sum(), and it gets a section of its own further down.

The last two lines show the difference between the ranges. range(1, 5) adds 1 + 2 + 3 + 4, and rangeClosed(1, 5) adds the 5 as well. IntStream.range(0, n) visits the same numbers as the loop for (int i = 0; i < n; i++).

Two sources you have met already
Files.lines in Path and Files gives a Stream<String> of a file's lines, and Matcher.results() in regular expressions gives a stream of every match. Everything on this page works on them too.
Quick check
What does IntStream.range(1, 5).sum() return?

Streams are lazy#

Calling filter or map does not filter or map anything yet. Before reading on, predict what this program prints. Its map step prints a line for every name it changes, and nothing comes after map.

Predict the outputNoTerminal.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> names = List.of("ada", "ben", "cleo");
6
7 names.stream()
8 .map(name -> {
9 System.out.println("map " + name);
10 return name.toUpperCase();
11 });
12
13 System.out.println("done");
14 }
15}

Which output does this program produce?

It prints only done. The pipeline has no terminal operation, so it never runs, and neither does the lambda inside map. An intermediate operation only adds a step to a plan. This is what it means for streams to be lazy: the work waits until a terminal operation needs a result. The next program shows the moment the work starts.

Lazy.java
1import java.util.List;
2import java.util.stream.Stream;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> names = List.of("ada", "ben", "cleo");
7
8 Stream<String> shouting = names.stream()
9 .map(name -> {
10 System.out.println("map " + name);
11 return name.toUpperCase();
12 });
13 System.out.println("The pipeline is built.");
14
15 List<String> result = shouting.toList();
16 System.out.println(result);
17 }
18}
Prints
›The pipeline is built.
›map ada
›map ben
›map cleo
›[ADA, BEN, CLEO]

The first line printed is The pipeline is built., even though map comes earlier in the code. map ada, map ben and map cleo appear only when toList() runs, and then [ADA, BEN, CLEO].

Think of plumbing. The source is a water tank, each intermediate operation is a length of pipe that does something to the water passing through it, and the terminal operation is the tap. Fitting the pipes together moves no water; only opening the tap does.

One element at a time

The next question is the order of the work. This pipeline prints from inside filter and from inside map. Pick the output you expect.

Predict the outputOrder.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> words = List.of("sun", "moon", "sky", "star");
6
7 List<String> result = words.stream()
8 .filter(w -> {
9 System.out.println("filter " + w);
10 return w.length() == 3;
11 })
12 .map(w -> {
13 System.out.println("map " + w);
14 return w.toUpperCase();
15 })
16 .toList();
17
18 System.out.println(result);
19 }
20}

In what order do the filter and map lines come out?

It is the second one. Java takes sun from the list and runs filter on it. sun passes, so it goes straight on through map and into the list as SUN, all before Java even looks at moon. moon fails the filter, so map never runs for it. Then sky makes the whole trip, and star is dropped.

Key idea

A stream does not run filter over the whole list and then map over what is left. It takes one element, sends it through every step, and only then takes the next. An element that a step drops never reaches the steps after it.

Because elements pass along one at a time, a stream needs no list between steps such as filter and map. A version with two loops, one that filtered everything into a new list and one that mapped that list into another, would build an extra list along the way.

Watch it happen

The lab runs a pipeline over six test scores: keep the scores of 60 or more, add 5 bonus points to each, and collect the results. Press Next step and watch the grid. Each column is one score and each row is one step. The grid fills one column at a time, top to bottom: one element's whole trip before the next one starts. Then press Add .limit(2) and run it again.

One element at a time6 test scores
List<Integer> result = scores.stream()
.filter(s -> s >= 60)
.map(s -> s + 5)
.toList();
Each score in the list, and what each step of the pipeline did with it so far
list724195588860
filter
map
toList

✓ kept by filter, × dropped by filter.

filter ran
0 times
map ran
0 times
toList() holds
[]

Nothing has run yet: filter and map wait for the terminal operation. Press Next step to run toList(), which starts the stream.

Here is the same pipeline with limit(2), as a program you can step through. Each lambda prints before it does its work, so the output is the order of the calls.

Limit.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> scores = List.of(72, 41, 95, 58, 88, 60);
6
7 List<Integer> result = scores.stream()
8 .filter(s -> {
9 System.out.println("filter " + s);
10 return s >= 60;
11 })
12 .map(s -> {
13 System.out.println("map " + s);
14 return s + 5;
15 })
16 .limit(2)
17 .toList();
18
19 System.out.println(result);
20 }
21}
Prints
›filter 72
›map 72
›filter 41
›filter 95
›map 95
›[77, 100]

It prints filter 72, map 72, filter 41, filter 95, map 95 and then [77, 100]. Open it in the playground, delete the .limit(2) line and run it again: the output grows to ten lines and ends with [77, 100, 93, 65].

Stopping early: limit and findFirst

limit(n) is an intermediate operation that lets the first n elements through and then wants no more. As soon as the second score has passed it, the stream stops taking scores from the list. 58, 88 and 60 are never looked at, so filter runs 3 times instead of 6, and map twice instead of 4 times.

An operation that can finish without visiting every element is called short-circuiting. Some terminal operations work this way too. findFirst() takes the first element that reaches it and stops right there.

FindFirst.java
1import java.util.List;
2import java.util.Optional;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> songs = List.of("Intro", "Blue Sky", "Echo", "Blue Moon");
7
8 Optional<String> firstBlue = songs.stream()
9 .filter(song -> {
10 System.out.println("checking " + song);
11 return song.startsWith("Blue");
12 })
13 .findFirst();
14
15 System.out.println(firstBlue);
16 System.out.println(firstBlue.orElse("no blue song"));
17 }
18}
Prints
›checking Intro
›checking Blue Sky
›Optional[Blue Sky]
›Blue Sky

It prints checking Intro and checking Blue Sky, and the checking stops there: Echo and Blue Moon are never looked at. The last two lines are Optional[Blue Sky] and Blue Sky.

findFirst() returns an Optional, a small box that holds the answer, or is empty when no element got through. That is why printing it shows Optional[Blue Sky]. orElse("no blue song") takes the value out of the box, or gives you the text you passed when the box is empty.

Optional has a lesson of its own
Methods that might have no answer return an Optional instead of null, and Optional: a value that might be missing covers the type properly. On this page, orElse is all you need.

Three more terminal operations stop as soon as they know their answer, and each returns a boolean:

  • anyMatch(test) returns true as soon as one element passes the test.
  • allMatch(test) returns false as soon as one element fails it.
  • noneMatch(test) returns false as soon as one element passes it.

peek: watching elements go by

Printing from inside filter and map works, but it clutters the lambdas. peek is an intermediate operation made for watching: it runs an action on each element that passes, and hands the element on unchanged. The documentation describes it as mainly a debugging aid, for seeing which elements reach a given point in a pipeline.

Peek.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> words = List.of("sun", "moon", "sky", "star");
6
7 List<String> result = words.stream()
8 .peek(w -> System.out.println("from the list: " + w))
9 .filter(w -> w.length() == 3)
10 .peek(w -> System.out.println("passed filter: " + w))
11 .map(w -> w.toUpperCase())
12 .toList();
13
14 System.out.println(result);
15 }
16}
Prints
›from the list: sun
›passed filter: sun
›from the list: moon
›from the list: sky
›passed filter: sky
›from the list: star
›[SUN, SKY]

It shows the same journeys as before. from the list: sun is followed straight away by passed filter: sun, while moon appears once and goes no further. The last line is [SUN, SKY].

peek is for looking, not for doing
Never put the real work of a program in peek, such as adding to a list or changing a field. A stream may skip it for elements it does not need, and the box below shows a case where it skips peek for every element.

Combining the elements into one value#

count() boils a whole stream down to one number. reduce is the general tool for that. You give it a starting value and a lambda that combines the result so far with the next element, and it returns the final result. Here it adds up four prices. Step through it and read the narration for each call.

Total.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> prices = List.of(4, 12, 7, 3);
6
7 int total = prices.stream()
8 .reduce(0, (sum, price) -> sum + price);
9
10 System.out.println("Total: " + total);
11 }
12}
Prints
›Total: 26

It prints Total: 26. The lambda (sum, price) -> sum + price runs once for each price, and whatever it returns becomes sum for the next call.

CallsumpriceReturns
1044
241216
316723
423326
sum starts at 0, the first argument of reduce, and the value returned by the last call is the result.

The starting value is called the identity, because it must change nothing when it is combined with an element: 0 for adding, 1 for multiplying, the empty string for joining text. Leave it out, and reduce starts from the first element instead. An empty stream has no first element, though, so that form returns an Optional, the same box findFirst() uses.

ReduceMore.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> prices = List.of(4, 12, 7, 3);
6 List<Integer> noPrices = List.of();
7
8 System.out.println(prices.stream().reduce(1, (a, b) -> a * b));
9 System.out.println(prices.stream().reduce((a, b) -> a + b));
10 System.out.println(noPrices.stream().reduce((a, b) -> a + b));
11 System.out.println(noPrices.stream().reduce(0, (a, b) -> a + b));
12 }
13}
Prints
›1008
›Optional[26]
›Optional.empty
›0

It prints 1008, the four prices multiplied together, then Optional[26]. For the empty list, the form without a starting value prints Optional.empty, and the form with one prints 0: the sum of no prices is 0.

Number streams: mapToInt, sum and average

Adding numbers up is so common that you might expect a sum() method on every stream. There is none. A Stream can hold any type of element, and adding up students or strings makes no sense. Turning the students into their scores with map does not help either:

Main.java
read along
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42));
8
9 int total = students.stream()
10 .map(s -> s.score())
11 .sum();
12 System.out.println(total);
13 }
14}
15
16record Student(String name, int score) {}
Terminal
read along
1$ javac Main.java
2Main.java:11: error: cannot find symbol
3 .sum();
4 ^
5 symbol: method sum()
6 location: interface Stream<Integer>
71 error

map boxes each int score into an Integer, so the result is a Stream<Integer>, and the compiler says that Stream<Integer> has no method sum(). The fix is mapToInt. It turns each element into a plain int and gives you an IntStream, which has the number methods.

Scores.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42),
8 new Student("Cleo", 67),
9 new Student("Dan", 38),
10 new Student("Eve", 82));
11
12 long passed = students.stream().filter(s -> s.score() >= 50).count();
13 int total = students.stream().mapToInt(s -> s.score()).sum();
14 System.out.println(passed + " passed, " + total + " points in all");
15
16 System.out.println(students.stream().mapToInt(s -> s.score()).average());
17 System.out.println(students.stream().mapToInt(s -> s.score()).max());
18 }
19}
20
21record Student(String name, int score) {}
Prints
›3 passed, 320 points in all
›OptionalDouble[64.0]
›OptionalInt[91]

It prints 3 passed, 320 points in all, then OptionalDouble[64.0] and OptionalInt[91]. The average and the maximum come in boxes for the same reason as before: a stream with no numbers in it has no average and no maximum. The sum of no numbers is 0, so sum() needs no box.

IntStream methodReturnsFor an empty stream
sum()int0
count()long0
average()OptionalDoubleOptionalDouble.empty
max() and min()OptionalIntOptionalInt.empty
LongStream and DoubleStream, made with mapToLong and mapToDouble, have the same methods for long and double values.

To get a plain number out of an OptionalDouble, call orElse with the number you want when the stream is empty.

Average.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42),
8 new Student("Cleo", 67),
9 new Student("Dan", 38),
10 new Student("Eve", 82));
11
12 double passAverage = students.stream()
13 .filter(s -> s.score() >= 50)
14 .mapToInt(s -> s.score())
15 .average()
16 .orElse(0);
17 System.out.println("Average of passes: " + passAverage);
18
19 double topAverage = students.stream()
20 .filter(s -> s.score() >= 95)
21 .mapToInt(s -> s.score())
22 .average()
23 .orElse(0);
24 System.out.println("Average of scores over 95: " + topAverage);
25 }
26}
27
28record Student(String name, int score) {}
Prints
›Average of passes: 80.0
›Average of scores over 95: 0.0

It prints Average of passes: 80.0, the average of 91, 67 and 82. Nobody scored 95 or more, so the second stream is empty and orElse(0) gives 0.0.

sum() can overflow
IntStream.sum() adds in int arithmetic, so a total past 2147483647 wraps around to a negative number without any error, the overflow from wrapper classes. When totals can grow that large, use mapToLong, whose sum() returns a long.
Quick check
Why does students.stream().map(s -> s.score()).sum() fail to compile?
Put the lines in orderSale.java
Items that cost more than 10 are on sale with 2 off. Put the lines in order so that the program adds up the sale prices and prints Sale total: 50.

Tap a line, then tap where it should go. Or use the arrows.

Method references#

Many lambdas in a pipeline do nothing except pass their argument to one method: s -> s.name(), name -> System.out.println(name). A method reference names that method with two colons instead, and Java builds the lambda for you. You have met two kinds already. Math::max names a static method, and Song::artist in Comparable, Comparator and sorting names a method to call on whichever song it is given. There are four kinds in all.

KindMethod referenceThe lambda it stands for
A static methodInteger::parseInttext -> Integer.parseInt(text)
A method of one particular objectSystem.out::printlnx -> System.out.println(x)
A method of the element itselfString::toUpperCases -> s.toUpperCase()
A constructorSong::newtitle -> new Song(title)
The Java Tutorials call the third kind a reference to an instance method of an arbitrary object of a particular type.
References.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<String> typed = List.of("12", "7", "30");
6 List<Integer> numbers = typed.stream()
7 .map(Integer::parseInt)
8 .toList();
9 System.out.println(numbers);
10
11 List<String> names = List.of("ada", "ben", "cleo");
12 names.stream()
13 .map(String::toUpperCase)
14 .forEach(System.out::println);
15
16 List<Song> songs = List.of("Intro", "Echo").stream()
17 .map(Song::new)
18 .toList();
19 System.out.println(songs);
20 }
21}
22
23record Song(String title) {}
Prints
›[12, 7, 30]
›ADA
›BEN
›CLEO
›[Song[title=Intro], Song[title=Echo]]

It prints [12, 7, 30], then ADA, BEN and CLEO on separate lines, then [Song[title=Intro], Song[title=Echo]]. Here is what each reference does with an element:

  • Integer::parseInt names a static method, so each element is passed to it as the argument: Integer.parseInt("12").
  • System.out::println names a method of one object that already exists, System.out. Each element becomes the argument: System.out.println("ADA").
  • String::toUpperCase has a class name before the colons, not an object, and an instance method after them. So the element itself becomes the object the method runs on: "ada".toUpperCase().
  • Song::new names the constructor of the Song record, so each title becomes new Song("Intro").

The accessors of a record work like String::toUpperCase. Here are the students again, with a method reference wherever a lambda only passed its argument on.

PassedRefs.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Student> students = List.of(
6 new Student("Ada", 91),
7 new Student("Ben", 42),
8 new Student("Cleo", 67),
9 new Student("Dan", 38),
10 new Student("Eve", 82));
11
12 students.stream()
13 .filter(s -> s.score() >= 50)
14 .map(Student::name)
15 .forEach(System.out::println);
16
17 int total = students.stream()
18 .mapToInt(Student::score)
19 .sum();
20 System.out.println("Total: " + total);
21 }
22}
23
24record Student(String name, int score) {}
Prints
›Ada
›Cleo
›Eve
›Total: 320

It prints Ada, Cleo and Eve on separate lines, then Total: 320. Student::name stands for s -> s.name(), and Student::score for s -> s.score().

When to use one
Use a method reference when a lambda only passes its arguments on to one method. The filter s -> s.score() >= 50 also compares the score with 50, so it stays a lambda. And when a lambda reads more clearly to you, keep the lambda: both do the same job.
Quick check
Which method reference does the same job as word -> word.length()?
ExercisePlaylist.java
Use mapToInt with the method reference Song::seconds to work out the total length of the playlist and the average song length. The program should print Total: 643 seconds and Average: 160.75 seconds.

A stream can be used once#

A stream is a one-way trip over its source. Once a terminal operation has run, the stream is used up, and asking it for anything more throws an exception. Run this and read the error.

Reuse.java
1import java.util.List;
2import java.util.stream.Stream;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> songs = List.of("Intro", "Echo", "Outro");
7 Stream<String> stream = songs.stream();
8
9 stream.forEach(song -> System.out.println(song));
10 long count = stream.count();
11 System.out.println(count + " songs");
12 }
13}
Prints
›Intro
›Echo
›Outro
!Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
! at Main.main(Main.java:10)
Stops on line 10

IllegalStateException: stream has already been operated upon or closed

A method was called at a moment when the object could not handle it.

The three songs print, and then stream.count() throws an IllegalStateException with the message stream has already been operated upon or closed. The forEach on the line before used the stream up.

The fix is to ask the list for a new stream for each job. A list can be streamed as often as you like, and each call to songs.stream() starts a new trip.

Reuse.java, fixed
read along
1songs.stream().forEach(song -> System.out.println(song));
2long count = songs.stream().count();

The rule covers intermediate operations too. Calling filter on a stream uses that stream, so a later map on the same stream variable throws the same exception. Writing each pipeline as one chain, from the source to the terminal operation, avoids the problem completely, which is why you rarely see a stream kept in a variable.

Key idea

Each stream runs one pipeline. To go over the same elements again, start a new stream from the source.

Stream or loop?#

Streams do not replace loops. Some jobs read better as a pipeline and some read better as a loop. One limit decides many cases: a lambda cannot change a local variable of the method around it. Here is an attempt to add up bank account moves with forEach:

Main.java
read along
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> moves = List.of(50, -20, -40, 30);
6 int balance = 0;
7
8 moves.stream().forEach(move -> balance += move);
9 System.out.println(balance);
10 }
11}
Terminal
read along
1$ javac Main.java
2Main.java:8: error: local variables referenced from a lambda expression must be final or effectively final
3 moves.stream().forEach(move -> balance += move);
4 ^
51 error

A local variable used inside a lambda must be effectively final: given a value once and never changed after that. balance changes, so the compiler refuses. For the final balance alone, the stream answer is moves.stream().mapToInt(m -> m).sum(). But some jobs carry a value from each element to the next, and then a loop is the clearer tool. This one prints the balance after every move and stops at the first overdraft.

Balance.java
1import java.util.List;
2
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> moves = List.of(50, -20, -40, 30);
6 int balance = 0;
7
8 for (int move : moves) {
9 balance += move;
10 System.out.println("after " + move + ": " + balance);
11 if (balance < 0) {
12 System.out.println("Overdrawn, stopping here.");
13 break;
14 }
15 }
16 }
17}
Prints
›after 50: 50
›after -20: 30
›after -40: -10
›Overdrawn, stopping here.

It prints after 50: 50, after -20: 30 and after -40: -10, then Overdrawn, stopping here. Each step needs the balance left by the step before, prints a line, and may end the walk. A loop says all of that directly.

A stream reads better when
A loop reads better when
The job is a chain of keep, change and combine steps.
Each step needs a value carried over from the step before, like a running balance.
The result is a new list, a count, a total or one element found.
The walk has to stop because of something that happened earlier in it.
Each step fits in a short lambda or a method reference.
The body does several unrelated things, or calls a method that throws a checked exception such as IOException, which a stream's lambdas cannot pass on.
You want limit or findFirst to stop the work early.
You need each element's position as you go.
forEach that fills a list is a loop in disguise
You may see students.stream().filter(s -> s.score() >= 50).forEach(s -> names.add(s.name())), with names an empty ArrayList made beforehand. It compiles, because the variable names never changes; only the list it points at does. But it is a loop written the long way round, and the documentation of the stream package discourages lambdas with side effects like this one. Let the terminal operation build the list: .map(Student::name).toList().

Speed is rarely the reason to choose. A stream is not faster than a loop just because it is a stream, and for the lists in most programs the difference is too small to notice. Write the version that a reader will understand first.

Key idea

Use a stream when the job reads as keep, change and combine. Use a loop when each step depends on what the earlier steps did.

Try it

Answer each one in your head or on paper first, then open the answer.

1
In songs.stream().filter(s -> s.length() > 4).map(String::toUpperCase).toList(), where songs is a List<String>, name the source, the intermediate operations and the terminal operation. What type does each step give?
2
Rewrite the loop in this program as a stream that gives the same count.
3
What does this program print, and how many times does peek run?
4
Add up the squares of the numbers 1 to 10 (1, 4, 9, and so on up to 100) with one pipeline that starts from IntStream.rangeClosed. What is the total?
5
Write each lambda as a method reference, or say why it cannot be one: s -> s.trim(), n -> System.out.println(n), (a, b) -> Integer.max(a, b), name -> new Student(name, 0).
6
Reuse.java threw an IllegalStateException. Fix it so that it prints the three songs and then 3 songs.

Practice

Work through these on your own. Each one builds on this page.

1
Split the sentence "the quick brown fox jumps over the lazy dog" into words with split(" "). With Arrays.stream on the array, print how many words there are, how many have more than 3 letters, and how many letters there are in total.
2
With the five students from this page, print whether anyone scored over 90 and whether everyone scored over 30, using anyMatch and allMatch. Then predict what each one returns for an empty list of students, and check.
3
Print the 7 times table, from 7 x 1 = 7 to 7 x 10 = 70, one line each, using IntStream.rangeClosed and forEach. Then write it as a for loop, and decide which version you would rather read.
4
Take the scores pipeline from the lab and add .peek(s -> System.out.println("from the list " + s)) straight after stream(). Predict the output with and without limit(2), then run both. Then move limit(2) to just after stream(), so it comes before filter, and predict again.
5
Find a loop you wrote for an earlier lesson, such as the sum and average of an array from arrays, and rewrite it as a stream. In two sentences, say which version is clearer and why.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email