Ламбди
Проблем
Шест реда за едно сравнение
Поръчките трябва да се подредят по сума. От модул „Колекции“ знаеш как: sort с Comparator, написан като анонимен клас.
class Order { private String customer; private double total;
public Order(String customer, double total) { this.customer = customer; this.total = total; }
public String getCustomer() { return customer; }
public double getTotal() { return total; }}List<Order> orders = new ArrayList<>(List.of( new Order("Мария", 75.50), new Order("Георги", 12.90), new Order("Елена", 40.00)));
orders.sort(new Comparator<Order>() { @Override public int compare(Order a, Order b) { return Double.compare(a.getTotal(), b.getTotal()); }});
for (Order o : orders) { System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());}Георги: 12.90 €Елена: 40.00 €Мария: 75.50 €Работи. Но от шестте реда на анонимния клас само един казва нещо: Double.compare(a.getTotal(), b.getTotal()). Останалото е обвивка, която пишеш всеки път: new Comparator<Order>(), @Override, public int compare, скоби. Искаш да подадеш само правилото.
Идеята
Ламбда: само правилото
Същото подреждане, на един ред:
orders.sort((a, b) -> Double.compare(a.getTotal(), b.getTotal()));(a, b) -> Double.compare(a.getTotal(), b.getTotal()) се казва ламбда израз, накратко ламбда. Отляво на -> са параметрите, отдясно е тялото. Това е методът compare без име, без типове и без return.
Ламбдата има три вида запис:
- Един параметър може да е без скоби:
o -> o.getTotal() > 50. Без параметри е() -> .... - Типовете на параметрите обикновено се пропускат, защото компилаторът ги знае от
sort. Може и да ги напишеш:(Order a, Order b) -> .... - Тяло с няколко реда е във
{ }и тогаваreturnе задължителен.
Comparator<Order> byTotal = (a, b) -> Double.compare(a.getTotal(), b.getTotal());
Comparator<Order> byTotalLong = (Order a, Order b) -> { double diff = a.getTotal() - b.getTotal(); return diff < 0 ? -1 : diff > 0 ? 1 : 0;};
orders.sort(byTotalLong);for (Order o : orders) { System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());}Георги: 12.90 €Елена: 40.00 €Мария: 75.50 €Идеята
Функционален интерфейс
Откъде Java знае, че (a, b) -> ... е метод compare? От типа: sort очаква Comparator, а той има един абстрактен метод. Ламбдата става неговото тяло.
Интерфейс с точно един абстрактен метод се казва функционален интерфейс. Може да си направиш свой:
interface PriceRule { double apply(double price);}PriceRule student = price -> price * 0.8;PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));System.out.println(minusFive.apply(50));40.045.0student е обект от тип PriceRule. Викаш го с името на метода от интерфейса: student.apply(50). Две правила за цена, нито един клас, който ги наследява.PriceRule student = price -> price * 0.8;PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));System.out.println(minusFive.apply(50));Стъпка по стъпка, с думи
- Ламбдата е обект от тип PriceRule, а student сочи към него. Тялото ѝ още не се изпълнява.
- Второ правило, втори обект. Нито един клас не е написан.
- student.apply(50) подава 50 на ламбдата: price е 50, а price * 0.8 дава 40.0.
- minusFive.apply(50): същото число, друго правило, 45.0.
По-точно, за любопитните
- Анотацията
@FunctionalInterfaceнад интерфейса кара компилатора да провери, че методът е точно един. Ако добавиш втори абстрактен метод:
@FunctionalInterfaceinterface PriceRule { double apply(double price); String name();}Main.java:1: error: Unexpected @FunctionalInterface annotation@FunctionalInterface^ PriceRule is not a functional interface multiple non-overriding abstract methods found in interface PriceRule- Методите с тяло (
defaultиstatic) не се броят. Не се броят и методите отObject: вComparatorпишеboolean equals(Object obj), но той е функционален. - Ламбдата не е анонимен клас. В нея
thisе обектът, в който е написана, а не самата ламбда.
Идеята
Готовите: Predicate, Function, Consumer, Supplier
Не е нужно да пишеш интерфейс за всяко правило. В java.util.function има готови за най-честите случаи:
Predicate<T>: приемаT, връщаboolean. Методът еtest. Това е условие: „голяма ли е поръчката?“.Function<T, R>: приемаT, връщаR. Методът еapply. Превръща едно в друго: поръчка в етикет.Consumer<T>: приемаT, не връща нищо. Методът еaccept. Прави нещо с него: отпечатва го, записва го.Supplier<T>: не приема нищо, връщаT. Методът еget. Дава нов обект, когато поискаш.
Predicate<Order> big = o -> o.getTotal() >= 50;Function<Order, String> label = o -> o.getCustomer().toUpperCase();Consumer<String> print = text -> System.out.println("-> " + text);Supplier<Order> guest = () -> new Order("Гост", 0);
Order order = new Order("Мария", 75.50);System.out.println(big.test(order));print.accept(label.apply(order));System.out.println(guest.get().getCustomer());true-> МАРИЯГостand, or и negate („не“). Функциите се нареждат една след друга с andThen.По-точно, за любопитните
- Има варианти за два параметъра:
BiFunction<T, U, R>,BiPredicate<T, U>,BiConsumer<T, U>.UnaryOperator<T>еFunction<T, T>, аBinaryOperator<T>еBiFunction<T, T, T>. - За примитивните типове има
IntPredicate,IntFunction<R>,ToDoubleFunction<T>и други. С тяхintне се пакетира вIntegerпри всяко извикване. f.andThen(g)прави първоf, послеg.f.compose(g)прави обратното: първоg, послеf.
Пробвай
Сглобени условия
Какво ще отпечата програмата?
Predicate<Order> big = o -> o.getTotal() >= 50;Predicate<Order> maria = o -> o.getCustomer().equals("Мария");
Order small = new Order("Мария", 30);Order large = new Order("Георги", 80);
System.out.println(big.and(maria).test(small));System.out.println(big.or(maria).test(small));System.out.println(big.negate().test(large));big.and(maria) е false, а big.or(maria) е true, защото е на Мария. Поръчката на Георги е 80 €, голяма е, а negate() обръща отговора: false.Виж го
Списъците и речниците приемат ламбди
List<String> cart = new ArrayList<>(List.of("хляб", "", "мляко", " ", "кафе"));cart.removeIf(item -> item.isBlank());cart.forEach(item -> System.out.println(item));
Map<String, List<String>> byCity = new TreeMap<>();byCity.computeIfAbsent("Пловдив", city -> new ArrayList<>()).add("Мария");byCity.computeIfAbsent("София", city -> new ArrayList<>()).add("Георги");byCity.computeIfAbsent("Пловдив", city -> new ArrayList<>()).add("Елена");byCity.forEach((city, names) -> System.out.println(city + ": " + names));хлябмлякокафеПловдив: [Мария, Елена]София: [Георги]removeIf(условие)маха всички елементи, за които условието е вярно. Това еPredicate.forEach(действие)прави действието с всеки елемент. Това еConsumer.computeIfAbsent(ключ, функция)връща стойността за ключа. Ако я няма, вика функцията, слага резултата в речника и връща него. Така групирането по град става на един ред, безif (!byCity.containsKey(...)).forEachна речника приема ламбда с два параметъра: ключ и стойност.
TreeMap пази ключовете подредени, затова Пловдив е преди София.
Идеята
Препратка към метод: ::
s -> System.out.println(s). Тогава може да напишеш само кой е методът:List<String> names = List.of("мария", "георги");names.forEach(System.out::println);
Function<String, String> upper = String::toUpperCase;Function<String, Integer> parse = Integer::parseInt;Supplier<List<String>> newList = ArrayList::new;
System.out.println(upper.apply("елена"));System.out.println(parse.apply("42") + 1);List<String> list = newList.get();list.add("кафе");System.out.println(list);мариягеоргиЕЛЕНА43[кафе]Това се казва препратка към метод. Има четири вида:
- метод на конкретен обект:
System.out::printlnеs -> System.out.println(s); - метод на параметъра:
String::toUpperCaseеs -> s.toUpperCase(); - статичен метод:
Integer::parseIntеs -> Integer.parseInt(s); - конструктор:
ArrayList::newе() -> new ArrayList<>().
Ако ламбдата прави нещо повече, например s -> s.trim().toUpperCase(), остави я ламбда.
Виж го
Подреждане по няколко полета
Comparator.comparing приема функция, която вади ключа за сравнение, и прави компаратор от нея. Към него се добавя следващо правило с thenComparing, а reversed() обръща посоката.
Учениците са подредени по оценка, от най-високата, а при равни оценки по име:
List<Student> students = new ArrayList<>(List.of( new Student("Иван", 5.50), new Student("Ана", 6.00), new Student("Петър", 5.50), new Student("Борис", 4.75)));students.sort(Comparator.comparing(Student::getGrade).reversed() .thenComparing(Student::getName));Ана 6.0Иван 5.5Петър 5.5Борис 4.75Чете се отляво надясно: „сравни по оценка, обърни, после при равенство сравни по име“. Нито един if, нито един анонимен клас.
По-точно, за любопитните
- С ламбда вместо препратка
reversed()понякога не се компилира:
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());Main.java:26: error: cannot find symbol orders.sort(Comparator.comparing(o -> o.getTotal()).reversed()); ^ symbol: method getTotal() location: variable o of type ObjectКомпилаторът извежда типа на o отвътре навън. Когато стигне до .reversed(), той още не знае, че става дума за Order, и приема Object. Поправката е препратка или тип на параметъра:
orders.sort(Comparator.comparing(Order::getTotal).reversed());orders.sort(Comparator.comparing((Order o) -> o.getTotal()).reversed());comparingIntиcomparingDoubleправят същото без пакетиране вIntegerиDouble.Comparator.naturalOrder()иComparator.reverseOrder()подреждат по естествения ред на типа: числа по големина, текст по азбука.
Пробвай
Какво обръща reversed()?
reversed() е в края, след thenComparing.Какво ще отпечата програмата?
List<Student> students = new ArrayList<>(List.of( new Student("Иван", 5.50), new Student("Ана", 6.00), new Student("Петър", 5.50), new Student("Борис", 4.75)));
students.sort(Comparator.comparing(Student::getGrade) .thenComparing(Student::getName) .reversed());students.forEach(s -> System.out.println(s.getName() + " " + s.getGrade()));reversed() обръща компаратора, върху който е извикан. Той вече е „по оценка, после по име“, затова се обръщат и двете: оценките са от най-високата, а Петър е преди Иван. Ако искаш само оценките наобратно, reversed() стои веднага след comparing(Student::getGrade), както в картата преди тази.Пробвай
Намери грешката
Избери реда с грешката.
Ламбдата променя total, а това не е позволено:
Main.java:9: error: local variables referenced from a lambda expression must be final or effectively final prices.forEach(p -> total += p); ^Ламбдата може да се изпълни по-късно и на друго място, затова получава копие на стойността на локалната променлива. Ако променливата можеше да се мени, копието и оригиналът щяха да се разминат. Затова Java позволява в ламбда само локални променливи, които не се променят след първата стойност. Те се казват ефективно final: могат и без думата final. Това важи и за промяна извън ламбдата: ако някъде по-долу пишеш total = 5, ламбдата пак не може да чете total.
За сбор ползвай обикновен цикъл. В следващия урок ще го направиш и на един ред.
По-точно, за любопитните
Да се чете такава променлива е позволено:
double rate = 0.2;PriceRule discount = price -> price * (1 - rate);System.out.println(discount.apply(100));80.0Полетата на обекта не са локални променливи и ламбдата може да ги променя. Това не е заобикаляне на правилото: ако ламбдите вървят в няколко нишки едновременно, общото поле е източник на грешки. За нишките е модул „Програми в истинския свят“.
Упражнение
Правила като стойности
Predicate, Function и останалите добави import java.util.function.*;.1. Подреждане по няколко полета. Клас Product с име, категория и цена. Подреди продуктите по категория, в една категория от най-скъпия, а при равна цена по име. Продуктите са: мишка, аксесоари, 25 €; лаптоп, компютри, 1299 €; клавиатура, аксесоари, 49,90 €; таблет, компютри, 399 €; слушалки, аксесоари, 49,90 €.
Аксесоари | Клавиатура | 49.90 €Аксесоари | Слушалки | 49.90 €Аксесоари | Мишка | 25.00 €Компютри | Лаптоп | 1299.00 €Компютри | Таблет | 399.00 €2. Филтър на поръчки. Добави към Order град. Напиши метод select(List<Order> orders, Predicate<Order> rule), който връща нов списък само с поръчките, за които правилото е вярно. Направи две правила, sofia и big (поне 50 €), и отпечатай поръчките с sofia.and(big), със sofia.negate() и с „София или поне 100 €“. Поръчките: Мария, София, 75,50 €; Георги, Варна, 12,90 €; Елена, София, 40 €; Димитър, Пловдив, 120 €.
София, над 50 €: Мария, София: 75.50 €Извън София: Георги, Варна: 12.90 € Димитър, Пловдив: 120.00 €Безплатна доставка (София или над 100 €): Мария, София: 75.50 € Елена, София: 40.00 € Димитър, Пловдив: 120.00 €Function<Double, Double>: студентска (−10%), за Черния петък (−20 €) и „не под нула“. Свържи ги с andThen в два реда: студентската първо и петъчната първо. Пробвай с 120 € и с 15 €. Редът има ли значение?120.00 € -> студент, после петък: 88.00 €; петък, после студент: 90.00 €15.00 € -> студент, после петък: 0.00 €; петък, после студент: 0.00 €Още задачи
- Чистене на списък. От списък с имейли махни празните и тези без
@с единremoveIf, после отпечатай останалите сforEach(System.out::println). - Ученици по клас. Групирай ученици по клас (
"10А","10Б") вTreeMap<String, List<String>>сcomputeIfAbsentи отпечатай речника сforEach. - Свой интерфейс. Направи
@FunctionalInterface interface Validator { boolean check(String value); }и три валидатора като ламбди: непразен текст, само цифри, дължина до 10. Пробвай ги на няколко низа.
Тест