Streams
Problem
A report in a loop
Orders now have a city too:
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);[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);[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 aPredicate.map(function)turns each element into another one. That is aFunction: it makes aStringfrom anOrder.sorted()sorts by the natural order, andsorted(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,sortedare intermediate operations: each one returns a new stream.toList,count,collect,forEachare 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 seecollect(Collectors.toList()). It does the same, but it does not promise exactly what kind of list it returns. If you need anArrayListthat you can change, writecollect(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
- Five prices. stream() only sets up the path: nothing has gone through yet.
- 12 does not pass p >= 50 and drops out. It never gets to map.
- 75 passes filter, map turns it into the text "€75", toList() collects it.
- 40 drops out too. Each element goes the whole way before the next one starts.
- 130 and 58 pass. The list prices does not change.
- 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());The stream is ready, nothing has gone through it yet.filter 12filter 75map 75filter 40[150]- The first line of the output is printed before a single element has gone through the stream.
map 75comes right afterfilter 75: the elements do not wait for all of them to go throughfilter.- 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);Sum of the even numbers: 6mapToInt(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);Orders over €50: 3Revenue: €312.40Average: €62.48Largest: €120.00count()gives the number of elements that got to the end. The type islong.average()andmax()do not return adoubledirectly. If there are no orders, there is no average. So they return "maybe a number", andorElse(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));[2, 3, 4, 5][Dimitar, Maria, Ana]Table 1Table 2Table 3distinct()removes the duplicates. It compares withequals.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 withsorted, that is a "top 3".IntStream.range(1, 4)gives the numbers 1, 2, 3. The end is not included, as infor (int i = 1; i < 4; i++). WithrangeClosed(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);distinct() leaves coffee, tea, water, juice. sorted() sorts them alphabetically: coffee, juice, tea, water. limit(3) keeps the first three, and map makes them uppercase.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"));Largest: Dimitarfalseno such orderifPresent(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"));Nickname: Mimino nicknameMore 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());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 computesx.orElseGet(() -> ...)calls the function only if there is no value. You notice the difference if the fallback is expensive to compute.average()ofIntStreamandDoubleStreamreturns anOptionalDouble, andmax()ofIntStreamreturns anOptionalInt. They are the same thing for primitive types:getAsDouble(),orElse(0).Optionalis for the result of a method. Do not use it as a field of a class or as a parameter.anyMatch,allMatchandnoneMatchanswer withtrueorfalsewhether any, all or none of the elements match the condition.
Try it
Find the bug
Pick the line with the bug.
A stream is a conveyor belt, not a list. count() starts it, and it gets used up. The second try on line 4 stops the program:
Count: 2Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closedIf you need two results, collect the filtered prices once with toList() and use the list: expensive.size() and expensive. Or make a new stream for each result with prices.stream().
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);{Plovdiv=1, Sofia=2, Varna=2}Plovdiv: €120.00Sofia: €115.50Varna: €76.90Ana, Dimitar, Elena, Georgi, Mariacollect collects the stream by a rule from the class Collectors:
groupingBy(Order::getCity, TreeMap::new, Collectors.counting()): group by city, put the groups in aTreeMap, count each group.summingDouble(Order::getTotal)instead ofcounting()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 aMap<String, List<Order>>: for each city, a list of its orders. - If you want a list of names rather than orders,
mappingtransforms 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);{Plovdiv=[Dimitar], Sofia=[Maria, Elena], Varna=[Georgi, Ana]}Collectors.partitioningBy(condition)splits into two groups:trueandfalse.Collectors.toMap(key, value)makes a map with one value per key and throws an exception if a key repeats.
Exercise
Reports with streams
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.
Drinks: €440.50Food: €96.00Sweets: €189.50Total: €726.002. 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().
Coffee beans: €314.50Tea: €126.00Chocolate: €112.003. 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).
4.50 5.75 6.00 3.00 5.50Average: 4.95Highest: 6.00Excellent: 34. 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}]+").
Coffee with milk, coffee without sugar. Tea with honey!coffee, honey, milk, sugar, tea, with, withoutMore 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
groupingByandCollectors.averagingDouble. - Big and small orders. With
anyMatch, check whether there is an order over €1000, and withallMatchwhether all of them are at least €10.
Test