Skip to content
wannacode

Streams

Problem

A report in a loop

Orders now have a city too:

Order
class Order {
private String customer;
private String city;
private double total;
public Order(String customer, String city, double total) {
this.customer = customer;
this.city = city;
this.total = total;
}
public String getCustomer() {
return customer;
}
public String getCity() {
return city;
}
public double getTotal() {
return total;
}
}
List<Order> orders = List.of(
new Order("Maria", "Sofia", 75.50),
new Order("Georgi", "Varna", 12.90),
new Order("Elena", "Sofia", 40.00),
new Order("Dimitar", "Plovdiv", 120.00),
new Order("Ana", "Varna", 64.00));

You want the customers with orders of at least €50, in alphabetical order:

List<String> result = new ArrayList<>();
for (Order o : orders) {
if (o.getTotal() >= 50) {
result.add(o.getCustomer());
}
}
Collections.sort(result);
System.out.println(result);
Output
[Ana, Dimitar, Maria]

Eight lines, and the question is just one. The code says how: make an empty list, loop, check, add, sort. To understand what it does, you have to run it in your head. Add "only the first three" or "no duplicates", and the loop will grow.

The idea

A stream: a series of steps

The same report, written as steps:

List<String> result = orders.stream()
.filter(o -> o.getTotal() >= 50)
.map(Order::getCustomer)
.sorted()
.toList();
System.out.println(result);
Output
[Ana, Dimitar, Maria]

You read it from top to bottom: take the orders, keep the ones of at least €50, take the customer, sort, collect into a list.

orders.stream() makes a stream from the list. A stream does not store data. It is a conveyor belt that the elements go along one at a time. Each step takes a lambda from the previous lesson:

  • filter(condition) lets through only the elements for which the condition is true. That is a Predicate.
  • map(function) turns each element into another one. That is a Function: it makes a String from an Order.
  • sorted() sorts by the natural order, and sorted(comparator) by your rule.
  • toList() collects whatever got to the end into a new list.

The list orders does not change. The stream only reads it.

More precisely, if you are curious
  • filter, map, sorted are intermediate operations: each one returns a new stream. toList, count, collect, forEach are terminal: they ask for a result and start the conveyor belt.
  • toList() is from Java 16 and returns a list that cannot be changed. In older code you will see collect(Collectors.toList()). It does the same, but it does not promise exactly what kind of list it returns. If you need an ArrayList that you can change, write collect(Collectors.toCollection(ArrayList::new)).
  • You can also make a stream from an array: Arrays.stream(prices), and from separate values: Stream.of("coffee", "tea").

See it

What gets through the filter

List<Integer> prices = List.of(12, 75, 40, 130, 58);
List<String> labels = prices.stream()
.filter(p -> p >= 50)
.map(p -> "€" + p)
.toList();
System.out.println(labels);
Step by step, in words
  1. Five prices. stream() only sets up the path: nothing has gone through yet.
  2. 12 does not pass p >= 50 and drops out. It never gets to map.
  3. 75 passes filter, map turns it into the text "€75", toList() collects it.
  4. 40 drops out too. Each element goes the whole way before the next one starts.
  5. 130 and 58 pass. The list prices does not change.
  6. The result is a new list with three strings.

The five prices go along the conveyor belt one at a time. 12 gets to filter and drops out. 75 passes, map turns it into the text "€75", and it goes into the list. Then 40 drops out, and 130 and 58 pass.

Each element goes the whole way before the next one starts. And nothing starts until the terminal operation toList() asks for a result.

More precisely, if you are curious

This is called lazy evaluation. The intermediate operations only record what needs to be done. See it with a print in every step:

Stream<Integer> stream = List.of(12, 75, 40).stream()
.filter(p -> {
System.out.println("filter " + p);
return p >= 50;
})
.map(p -> {
System.out.println("map " + p);
return p * 2;
});
System.out.println("The stream is ready, nothing has gone through it yet.");
System.out.println(stream.toList());
Output
The stream is ready, nothing has gone through it yet.
filter 12
filter 75
map 75
filter 40
[150]
  • The first line of the output is printed before a single element has gone through the stream.
  • map 75 comes right after filter 75: the elements do not wait for all of them to go through filter.
  • That is why limit(3) stops the conveyor belt as soon as three elements get to the end. The rest are not even checked.
  • parallelStream() runs the elements in several threads at the same time. It helps with a lot of data and heavy computations. With small lists it is slower, and the lambdas must not touch shared data. Threads are in the module "Real-world programs".

The idea

Sum, count, average

A stream can end with a number instead of a list. The sum of the even numbers:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream().filter(n -> n % 2 == 0).mapToInt(n -> n).sum();
System.out.println("Sum of the even numbers: " + sum);
Output
Sum of the even numbers: 6
mapToInt(n -> n) makes a stream of int from a Stream<Integer>. It is called an IntStream and has sum(), average(), max() and min(). For double there are mapToDouble and DoubleStream.

The same for the orders:

long big = orders.stream().filter(o -> o.getTotal() >= 50).count();
double revenue = orders.stream().mapToDouble(Order::getTotal).sum();
double average = orders.stream().mapToDouble(Order::getTotal).average().orElse(0);
double maxTotal = orders.stream().mapToDouble(Order::getTotal).max().orElse(0);
System.out.println("Orders over €50: " + big);
System.out.printf("Revenue: €%.2f%n", revenue);
System.out.printf("Average: €%.2f%n", average);
System.out.printf("Largest: €%.2f%n", maxTotal);
Output
Orders over €50: 3
Revenue: €312.40
Average: €62.48
Largest: €120.00
  • count() gives the number of elements that got to the end. The type is long.
  • average() and max() do not return a double directly. If there are no orders, there is no average. So they return "maybe a number", and orElse(0) says what it should be if there is none. That is the next idea.

The idea

Sort, cut, no duplicates

List<Integer> ratings = List.of(5, 3, 5, 4, 2, 4, 5);
System.out.println(ratings.stream().distinct().sorted().toList());
List<String> top = orders.stream()
.sorted(Comparator.comparing(Order::getTotal).reversed())
.limit(3)
.map(Order::getCustomer)
.toList();
System.out.println(top);
IntStream.range(1, 4).forEach(i -> System.out.println("Table " + i));
Output
[2, 3, 4, 5]
[Dimitar, Maria, Ana]
Table 1
Table 2
Table 3
  • distinct() removes the duplicates. It compares with equals.
  • sorted(Comparator.comparing(Order::getTotal).reversed()) sorts from the largest order, with a comparator from the previous lesson.
  • limit(3) lets through only the first three. Together with sorted, that is a "top 3".
  • IntStream.range(1, 4) gives the numbers 1, 2, 3. The end is not included, as in for (int i = 1; i < 4; i++). With rangeClosed(1, 4) it is.

Try it

What gets to the end?

What will the program print?

List<String> drinks = List.of("coffee", "tea", "coffee", "water", "tea", "juice");
List<String> result = drinks.stream()
.distinct()
.sorted()
.limit(3)
.map(String::toUpperCase)
.toList();
System.out.println(result);
Answer

The idea

Optional: a result that may be missing

Which order is the largest? If the list is empty, there is no answer. The method could return null, but then the caller has to remember to check. If they forget, the program stops with a NullPointerException somewhere later.

That is why max, min and findFirst return an Optional: a box that either holds a value or is empty.

Optional<Order> biggest = orders.stream().max(Comparator.comparing(Order::getTotal));
biggest.ifPresent(o -> System.out.println("Largest: " + o.getCustomer()));
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();
System.out.println(huge.isPresent());
System.out.println(huge.map(Order::getCustomer).orElse("no such order"));
Output
Largest: Dimitar
false
no such order
  • ifPresent(action) does the action only if there is a value.
  • isPresent() tells you whether there is one.
  • orElse(fallback) gives the value or the fallback.
  • map(function) transforms the value, if there is one. An empty box stays empty.

Optional also helps with your own methods that return null. Optional.ofNullable puts whatever it gets into the box: a value or nothing.

static String nickname(String user) {
// Returns the nickname, or null if the person has none.
return user.equals("maria") ? "Mimi" : null;
}
Optional<String> maria = Optional.ofNullable(nickname("maria"));
maria.ifPresent(name -> System.out.println("Nickname: " + name));
Optional<String> georgi = Optional.ofNullable(nickname("georgi"));
System.out.println(georgi.orElse("no nickname"));
Output
Nickname: Mimi
no nickname
More precisely, if you are curious
  • get() gives the value, but if the box is empty, it throws an exception:
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();
System.out.println(huge.get().getCustomer());
Error
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:143)

orElseThrow() does the same, but its name says honestly what will happen. Use it if an empty box is an error.

  • orElse(x) always computes x. orElseGet(() -> ...) calls the function only if there is no value. You notice the difference if the fallback is expensive to compute.
  • average() of IntStream and DoubleStream returns an OptionalDouble, and max() of IntStream returns an OptionalInt. They are the same thing for primitive types: getAsDouble(), orElse(0).
  • Optional is for the result of a method. Do not use it as a field of a class or as a parameter.
  • anyMatch, allMatch and noneMatch answer with true or false whether any, all or none of the elements match the condition.

Try it

Find the bug

The program should print how many prices are expensive, and the prices themselves. The first line comes out, then the program stops. Which line is the bug on?

Pick the line with the bug.

The idea

Grouping and joining

How many orders are there from each city, and what is the revenue?

Map<String, Long> countByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.counting()));
System.out.println(countByCity);
Map<String, Double> revenueByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.summingDouble(Order::getTotal)));
revenueByCity.forEach((city, sum) -> System.out.printf("%s: €%.2f%n", city, sum));
String customers = orders.stream()
.map(Order::getCustomer)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(customers);
Output
{Plovdiv=1, Sofia=2, Varna=2}
Plovdiv: €120.00
Sofia: €115.50
Varna: €76.90
Ana, Dimitar, Elena, Georgi, Maria

collect collects the stream by a rule from the class Collectors:

  • groupingBy(Order::getCity, TreeMap::new, Collectors.counting()): group by city, put the groups in a TreeMap, count each group.
  • summingDouble(Order::getTotal) instead of counting() adds up instead of counting.
  • joining(", ") joins the strings into one, with a comma between them.

TreeMap::new is a constructor reference. It says what kind of map groupingBy should make. Without it the result is a HashMap, and the order of the cities in it is not guaranteed.

More precisely, if you are curious
  • With just the function, groupingBy(Order::getCity), the result is a Map<String, List<Order>>: for each city, a list of its orders.
  • If you want a list of names rather than orders, mapping transforms each element before it goes into the group:
Map<String, List<String>> namesByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new,
Collectors.mapping(Order::getCustomer, Collectors.toList())));
System.out.println(namesByCity);
Output
{Plovdiv=[Dimitar], Sofia=[Maria, Elena], Varna=[Georgi, Ana]}
  • Collectors.partitioningBy(condition) splits into two groups: true and false. Collectors.toMap(key, value) makes a map with one value per key and throws an exception if a key repeats.

Exercise

Reports with streams

Solve the tasks in your editor. Import java.util.stream.Collectors, and for arrays java.util.Arrays.

1. Sales by category. A class Sale with a product, a category, a quantity and a unit price, and a method getRevenue() (quantity times price). Add up the revenue by category in a TreeMap and print the total revenue. The sales: coffee beans, drinks, 12 at €18.50; tea, drinks, 30 at €4.20; chocolate, sweets, 40 at €2.80; biscuits, sweets, 25 at €3.10; honey, food, 8 at €12; coffee beans again, 5 at €18.50.

Output
Drinks: €440.50
Food: €96.00
Sweets: €189.50
Total: €726.00

2. Top 3 products. With the same sales, find the three products with the highest revenue. The coffee is in two sales, so first group by product and add up, then sort the map's entries by value. Hint: map.entrySet().stream() and Map.Entry.<String, Double>comparingByValue().reversed().

Output
Coffee beans: €314.50
Tea: €126.00
Chocolate: €112.00

3. Average grade. Read one line of grades separated by spaces. Print the average, the highest and how many are excellent (5.50 and up). Hint: Arrays.stream(line.split(" ")).mapToDouble(Double::parseDouble).

Input
4.50 5.75 6.00 3.00 5.50
Output
Average: 4.95
Highest: 6.00
Excellent: 3

4. The words in a text. Read a line of text and print the words in it with no duplicates, in alphabetical order, in lowercase, separated by commas. Split on everything that is not a letter: split("[^\\p{L}]+").

Input
Coffee with milk, coffee without sugar. Tea with honey!
Output
coffee, honey, milk, sugar, tea, with, without

More tasks

  • The longest word. With max(Comparator.comparing(String::length)), find the longest word in a sentence. What will the program return for an empty line?
  • Average by class. Students with a name, a class and a grade: the average grade for each class with groupingBy and Collectors.averagingDouble.
  • Big and small orders. With anyMatch, check whether there is an order over €1000, and with allMatch whether all of them are at least €10.

Test

Check yourself

1. What happens to the list orders after orders.stream().filter(o -> o.getTotal() > 50).toList()?
2. Which of these is a terminal operation that starts the stream?
3. orders.stream().max(...) returns an Optional<Order>. Why not just an Order?
4. Why is the grouping in a TreeMap, and not just groupingBy(Order::getCity)?
5. Stream<Integer> s = list.stream(); s.count(); s.toList(); What happens?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·