Stream
Проблем
Отчет в цикъл
Поръчките вече имат и град:
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("Мария", "София", 75.50), new Order("Георги", "Варна", 12.90), new Order("Елена", "София", 40.00), new Order("Димитър", "Пловдив", 120.00), new Order("Ана", "Варна", 64.00));Искаш клиентите с поръчки поне 50 €, подредени по азбучен ред:
List<String> result = new ArrayList<>();for (Order o : orders) { if (o.getTotal() >= 50) { result.add(o.getCustomer()); }}Collections.sort(result);System.out.println(result);[Ана, Димитър, Мария]Осем реда, а въпросът е един. Кодът казва как: направи празен списък, обиколи, провери, добави, подреди. За да разбереш какво прави, трябва да го изпълниш наум. Добавиш ли „само първите трима“ или „без повторения“, цикълът ще расте.
Идеята
Поток: поредица от стъпки
Същият отчет, написан като стъпки:
List<String> result = orders.stream() .filter(o -> o.getTotal() >= 50) .map(Order::getCustomer) .sorted() .toList();System.out.println(result);[Ана, Димитър, Мария]Чете се отгоре надолу: вземи поръчките, остави тези поне за 50 €, вземи клиента, подреди, събери в списък.
orders.stream() прави поток (на английски stream) от списъка. Потокът не пази данни. Той е конвейер, по който елементите минават един по един. Всяка стъпка приема ламбда от предишния урок:
filter(условие)пропуска нататък само елементите, за които условието е вярно. Това еPredicate.map(функция)превръща всеки елемент в друг. Това еFunction: отOrderправиString.sorted()подрежда по естествения ред, аsorted(компаратор)по твоето правило.toList()събира каквото е стигнало до края в нов списък.
Списъкът orders не се променя. Потокът само го чете.
По-точно, за любопитните
filter,map,sortedса междинни операции: всяка връща нов поток.toList,count,collect,forEachса крайни: те искат резултат и пускат конвейера.toList()е от Java 16 и връща списък, който не може да се променя. В по-стар код ще видишcollect(Collectors.toList()). Той прави същото, но не обещава какъв точно списък ще върне. Ако ти трябваArrayList, който да променяш, пишиcollect(Collectors.toCollection(ArrayList::new)).- Поток се прави и от масив:
Arrays.stream(prices), и от отделни стойности:Stream.of("кафе", "чай").
Виж го
Какво минава през филтъра
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);Стъпка по стъпка, с думи
- Пет цени. stream() само подготвя пътя: още нищо не е минало.
- 12 не минава p >= 50 и отпада. До map не стига.
- 75 минава filter, map го превръща в текста „75 €“, toList() го събира.
- 40 също отпада. Всеки елемент минава целия път, преди да тръгне следващият.
- 130 и 58 минават. Списъкът prices не се променя.
- Резултатът е нов списък с три текста.
Петте цени тръгват по конвейера една по една. 12 стига до filter и отпада. 75 минава, map го прави на текста „75 €“ и той влиза в списъка. После 40 отпада, а 130 и 58 минават.
Всеки елемент изминава целия път, преди да тръгне следващият. И нищо не тръгва, докато крайната операция toList() не поиска резултат.
По-точно, за любопитните
Това се казва мързеливо изпълнение. Междинните операции само записват какво трябва да се направи. Виж го с отпечатване във всяка стъпка:
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("Потокът е готов, нищо не е минало през него.");System.out.println(stream.toList());Потокът е готов, нищо не е минало през него.filter 12filter 75map 75filter 40[150]- Първият ред на изхода е отпечатан, преди през потока да е минал и един елемент.
map 75е веднага следfilter 75: елементите не чакат всички да минат презfilter.- Затова
limit(3)спира конвейера, щом три елемента стигнат до края. Останалите дори не се проверяват. parallelStream()пуска елементите в няколко нишки едновременно. Помага при много данни и тежки сметки. При малки списъци е по-бавен, а ламбдите не трябва да пипат общи данни. За нишките е модул „Програми в истинския свят“.
Идеята
Сбор, брой, средно
Потокът може да завърши с число вместо със списък. Сборът на четните числа:
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);Сума на четните числа: 6mapToInt(n -> n) прави от Stream<Integer> поток от int. Той се казва IntStream и има sum(), average(), max() и min(). За double има mapToDouble и DoubleStream.Същото за поръчките:
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("Поръчки над 50 €: " + big);System.out.printf("Оборот: %.2f €%n", revenue);System.out.printf("Средно: %.2f €%n", average);System.out.printf("Най-голяма: %.2f €%n", maxTotal);Поръчки над 50 €: 3Оборот: 312.40 €Средно: 62.48 €Най-голяма: 120.00 €count()дава броя на елементите, които са стигнали до края. Типът еlong.average()иmax()не връщат направоdouble. Ако поръчки няма, средно няма. Затова връщат „може би число“, аorElse(0)казва какво да е, ако го няма. Това е следващата идея.
Идеята
Подреди, отрежи, без повторения
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("Маса " + i));[2, 3, 4, 5][Димитър, Мария, Ана]Маса 1Маса 2Маса 3distinct()маха повторенията. Сравнява сequals.sorted(Comparator.comparing(Order::getTotal).reversed())подрежда от най-голямата поръчка, с компаратор от предишния урок.limit(3)пропуска само първите три. Заедно съсsortedтова е „топ 3“.IntStream.range(1, 4)дава числата 1, 2, 3. Краят не се включва, като вfor (int i = 1; i < 4; i++). СrangeClosed(1, 4)се включва.
Пробвай
Кое стига до края?
Какво ще отпечата програмата?
List<String> drinks = List.of("кафе", "чай", "кафе", "вода", "чай", "сок");List<String> result = drinks.stream() .distinct() .sorted() .limit(3) .map(String::toUpperCase) .toList();System.out.println(result);distinct() оставя кафе, чай, вода, сок. sorted() ги подрежда по азбука: вода, кафе, сок, чай. limit(3) оставя първите три, а map ги прави с главни букви.Идеята
Optional: резултат, който може да липсва
Коя е най-голямата поръчка? Ако списъкът е празен, отговор няма. Методът може да върне null, но тогава викащият трябва да помни да провери. Забрави ли, програмата спира с NullPointerException някъде по-късно.
Затова max, min и findFirst връщат Optional: кутия, в която има стойност или е празна.
Optional<Order> biggest = orders.stream().max(Comparator.comparing(Order::getTotal));biggest.ifPresent(o -> System.out.println("Най-голяма: " + 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("няма такава"));Най-голяма: Димитърfalseняма такаваifPresent(действие)прави действието само ако има стойност.isPresent()казва дали има.orElse(резерва)дава стойността или резервата.map(функция)превръща стойността, ако я има. Празната кутия си остава празна.
Optional помага и с твои методи, които връщат null. Optional.ofNullable слага в кутията каквото получи: стойност или нищо.
static String nickname(String user) { // Връща прякора или null, ако човекът няма. return user.equals("maria") ? "Мими" : null;}Optional<String> maria = Optional.ofNullable(nickname("maria"));maria.ifPresent(name -> System.out.println("Прякор: " + name));
Optional<String> georgi = Optional.ofNullable(nickname("georgi"));System.out.println(georgi.orElse("без прякор"));Прякор: Мимибез прякорПо-точно, за любопитните
get()дава стойността, но ако кутията е празна, хвърля изключение:
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() прави същото, но името казва честно какво ще стане. Пиши него, ако празната кутия е грешка.
orElse(x)изчисляваxвинаги.orElseGet(() -> ...)вика функцията само ако стойност няма. Разликата се усеща, ако резервата е скъпа за смятане.average()наIntStreamиDoubleStreamвръщаOptionalDouble, аmax()наIntStreamвръщаOptionalInt. Те са същото за примитивни типове:getAsDouble(),orElse(0).Optionalе за резултат на метод. Не го слагай като поле на клас или като параметър.anyMatch,allMatchиnoneMatchотговарят сtrueилиfalseдали някой, всички или никой елемент не отговаря на условието.
Пробвай
Намери грешката
Избери реда с грешката.
Потокът е конвейер, а не списък. count() го пуска и той се изразходва. Вторият опит на ред 4 спира програмата:
Брой: 2Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closedАко ти трябват два резултата, събери филтрираните цени веднъж с toList() и ползвай списъка: expensive.size() и expensive. Или направи нов поток за всеки резултат с prices.stream().
Идеята
Групиране и съединяване
Колко поръчки има от всеки град и какъв е оборотът?
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);{Варна=2, Пловдив=1, София=2}Варна: 76.90 €Пловдив: 120.00 €София: 115.50 €Ана, Георги, Димитър, Елена, Марияcollect събира потока по правило от класа Collectors:
groupingBy(Order::getCity, TreeMap::new, Collectors.counting()): групирай по град, сложи групите вTreeMap, за всяка група преброй.summingDouble(Order::getTotal)вместоcounting()сумира вместо да брои.joining(", ")съединява низовете в един, със запетая между тях.
TreeMap::new е препратка към конструктор. Тя казва какъв речник да направи groupingBy. Без нея резултатът е HashMap, а в него редът на градовете не е гарантиран.
По-точно, за любопитните
- Само с функцията,
groupingBy(Order::getCity), резултатът еMap<String, List<Order>>: за всеки град списък с поръчките му. - Ако искаш списък с имена, а не с поръчки,
mappingпревръща всеки елемент, преди да влезе в групата:
Map<String, List<String>> namesByCity = orders.stream() .collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.mapping(Order::getCustomer, Collectors.toList())));System.out.println(namesByCity);{Варна=[Георги, Ана], Пловдив=[Димитър], София=[Мария, Елена]}Collectors.partitioningBy(условие)дели на две групи:trueиfalse.Collectors.toMap(ключ, стойност)прави речник с една стойност за ключ и хвърля изключение, ако ключът се повтори.
Упражнение
Отчети с потоци
java.util.stream.Collectors, а за масиви java.util.Arrays.1. Продажби по категория. Клас Sale с продукт, категория, брой и единична цена и метод getRevenue() (брой по цена). Сумирай оборота по категория в TreeMap и отпечатай общия оборот. Продажбите: кафе на зърна, напитки, 12 бр. по 18,50 €; чай, напитки, 30 по 4,20 €; шоколад, сладки, 40 по 2,80 €; бисквити, сладки, 25 по 3,10 €; мед, храни, 8 по 12 €; пак кафе на зърна, 5 по 18,50 €.
Напитки: 440.50 €Сладки: 189.50 €Храни: 96.00 €Общо: 726.00 €2. Топ 3 продукта. Със същите продажби намери трите продукта с най-голям оборот. Кафето е в две продажби, затова първо групирай по продукт и сумирай, после подреди записите на речника по стойност. Подсказка: map.entrySet().stream() и Map.Entry.<String, Double>comparingByValue().reversed().
Кафе на зърна: 314.50 €Чай: 126.00 €Шоколад: 112.00 €3. Средна оценка. Прочети един ред с оценки, разделени с интервал. Отпечатай средната, най-високата и колко са отличните (5,50 и нагоре). Подсказка: Arrays.stream(line.split(" ")).mapToDouble(Double::parseDouble).
4.50 5.75 6.00 3.00 5.50Средно: 4.95Най-висока: 6.00Отлични: 34. Думите в текст. Прочети ред текст и отпечатай думите в него без повторения, по азбучен ред, с малки букви, разделени със запетая. Раздели по всичко, което не е буква: split("[^\\p{L}]+").
Кафе с мляко, кафе без захар. Чай с мед!без, захар, кафе, мед, мляко, с, чайОще задачи
- Най-дългата дума. С
max(Comparator.comparing(String::length))намери най-дългата дума в изречение. Какво ще върне програмата за празен ред? - Средно по клас. Ученици с име, клас и оценка: средната оценка за всеки клас с
groupingByиCollectors.averagingDouble. - Големи и малки поръчки. С
anyMatchпровери дали има поръчка над 1000 €, а сallMatchдали всички са поне 10 €.
Тест