Lambdas
Problem
Six lines for one comparison
The orders need to be sorted by total. You know how from the "Collections" module: sort with a Comparator, written as an anonymous class.
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("Maria", 75.50), new Order("Georgi", 12.90), new Order("Elena", 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());}Georgi: €12.90Elena: €40.00Maria: €75.50It works. But of the six lines of the anonymous class, only one says something: Double.compare(a.getTotal(), b.getTotal()). The rest is wrapping you write every time: new Comparator<Order>(), @Override, public int compare, braces. You want to pass just the rule.
The idea
A lambda: just the rule
The same sort, on one line:
orders.sort((a, b) -> Double.compare(a.getTotal(), b.getTotal()));(a, b) -> Double.compare(a.getTotal(), b.getTotal()) is called a lambda expression, or a lambda for short. To the left of -> are the parameters, to the right is the body. This is the method compare with no name, no types and no return.
A lambda can be written in three ways:
- One parameter can go without parentheses:
o -> o.getTotal() > 50. With no parameters it is() -> .... - The types of the parameters are usually left out, because the compiler knows them from
sort. You can also write them:(Order a, Order b) -> .... - A body with several lines goes in
{ }, and thenreturnis required.
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());}Georgi: €12.90Elena: €40.00Maria: €75.50The idea
Functional interfaces
How does Java know that (a, b) -> ... is the method compare? From the type: sort expects a Comparator, and it has one abstract method. The lambda becomes its body.
An interface with exactly one abstract method is called a functional interface. You can make your own:
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 is an object of type PriceRule. You call it with the name of the method from the interface: student.apply(50). Two price rules, and not a single class that implements them.PriceRule student = price -> price * 0.8;PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));System.out.println(minusFive.apply(50));Step by step, in words
- The lambda is an object of type PriceRule, and student points to it. Its body does not run yet.
- A second rule, a second object. Not a single class is written.
- student.apply(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.0.
- minusFive.apply(50): the same number, a different rule, 45.0.
More precisely, if you are curious
- The annotation
@FunctionalInterfaceabove the interface makes the compiler check that there is exactly one method. If you add a second abstract method:
@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- Methods with a body (
defaultandstatic) do not count. Neither do the methods fromObject:Comparatordeclaresboolean equals(Object obj), but it is still functional. - A lambda is not an anonymous class. Inside it,
thisis the object it is written in, not the lambda itself.
The idea
The built-in ones: Predicate, Function, Consumer, Supplier
You do not need to write an interface for every rule. java.util.function has ready-made ones for the most common cases:
Predicate<T>: takes aT, returns aboolean. The method istest. It is a condition: "is the order big?".Function<T, R>: takes aT, returns anR. The method isapply. It turns one thing into another: an order into a label.Consumer<T>: takes aT, returns nothing. The method isaccept. It does something with it: prints it, saves it.Supplier<T>: takes nothing, returns aT. The method isget. It gives you a new object when you ask.
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("Guest", 0);
Order order = new Order("Maria", 75.50);System.out.println(big.test(order));print.accept(label.apply(order));System.out.println(guest.get().getCustomer());true-> MARIAGuestand, or and negate ("not"). Functions are chained one after another with andThen.More precisely, if you are curious
- There are versions for two parameters:
BiFunction<T, U, R>,BiPredicate<T, U>,BiConsumer<T, U>.UnaryOperator<T>isFunction<T, T>, andBinaryOperator<T>isBiFunction<T, T, T>. - For the primitive types there are
IntPredicate,IntFunction<R>,ToDoubleFunction<T>and others. With them, anintis not boxed into anIntegeron every call. f.andThen(g)doesffirst, theng.f.compose(g)does the opposite:gfirst, thenf.
Try it
Combined conditions
What will the program print?
Predicate<Order> big = o -> o.getTotal() >= 50;Predicate<Order> maria = o -> o.getCustomer().equals("Maria");
Order small = new Order("Maria", 30);Order large = new Order("Georgi", 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) is false, and big.or(maria) is true, because it is Maria's. Georgi's order is €80, it is big, and negate() flips the answer: false.See it
Lists and maps take lambdas
List<String> cart = new ArrayList<>(List.of("bread", "", "milk", " ", "coffee"));cart.removeIf(item -> item.isBlank());cart.forEach(item -> System.out.println(item));
Map<String, List<String>> byCity = new TreeMap<>();byCity.computeIfAbsent("Plovdiv", city -> new ArrayList<>()).add("Maria");byCity.computeIfAbsent("Sofia", city -> new ArrayList<>()).add("Georgi");byCity.computeIfAbsent("Plovdiv", city -> new ArrayList<>()).add("Elena");byCity.forEach((city, names) -> System.out.println(city + ": " + names));breadmilkcoffeePlovdiv: [Maria, Elena]Sofia: [Georgi]removeIf(condition)removes all elements for which the condition is true. That is aPredicate.forEach(action)does the action with each element. That is aConsumer.computeIfAbsent(key, function)returns the value for the key. If there is none, it calls the function, puts the result in the map and returns it. This way grouping by city takes one line, with noif (!byCity.containsKey(...)).- The map's
forEachtakes a lambda with two parameters: the key and the value.
TreeMap keeps the keys sorted, which is why Plovdiv comes before Sofia.
The idea
Method references: ::
s -> System.out.println(s). Then you can write just which method it is:List<String> names = List.of("maria", "georgi");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("elena"));System.out.println(parse.apply("42") + 1);List<String> list = newList.get();list.add("coffee");System.out.println(list);mariageorgiELENA43[coffee]This is called a method reference. There are four kinds:
- a method of a specific object:
System.out::printlniss -> System.out.println(s); - a method of the parameter:
String::toUpperCaseiss -> s.toUpperCase(); - a static method:
Integer::parseIntiss -> Integer.parseInt(s); - a constructor:
ArrayList::newis() -> new ArrayList<>().
If the lambda does something more, for example s -> s.trim().toUpperCase(), leave it as a lambda.
See it
Sorting by several fields
Comparator.comparing takes a function that extracts the key to compare by, and makes a comparator from it. You add the next rule to it with thenComparing, and reversed() flips the direction.
The students are sorted by grade, from the highest, and by name when the grades are equal:
List<Student> students = new ArrayList<>(List.of( new Student("Ivan", 5.50), new Student("Ana", 6.00), new Student("Peter", 5.50), new Student("Boris", 4.75)));students.sort(Comparator.comparing(Student::getGrade).reversed() .thenComparing(Student::getName));Ana 6.0Ivan 5.5Peter 5.5Boris 4.75You read it from left to right: "compare by grade, reverse, then compare by name when they are equal". Not a single if, not a single anonymous class.
More precisely, if you are curious
- With a lambda instead of a reference,
reversed()sometimes does not compile:
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 ObjectThe compiler infers the type of o from the inside out. When it gets to .reversed(), it does not know yet that this is about Order, so it assumes Object. The fix is a reference or a type on the parameter:
orders.sort(Comparator.comparing(Order::getTotal).reversed());orders.sort(Comparator.comparing((Order o) -> o.getTotal()).reversed());comparingIntandcomparingDoubledo the same without boxing intoIntegerandDouble.Comparator.naturalOrder()andComparator.reverseOrder()sort by the natural order of the type: numbers by size, text alphabetically.
Try it
What does reversed() reverse?
reversed() is at the end, after thenComparing.What will the program print?
List<Student> students = new ArrayList<>(List.of( new Student("Ivan", 5.50), new Student("Ana", 6.00), new Student("Peter", 5.50), new Student("Boris", 4.75)));
students.sort(Comparator.comparing(Student::getGrade) .thenComparing(Student::getName) .reversed());students.forEach(s -> System.out.println(s.getName() + " " + s.getGrade()));reversed() reverses the comparator it is called on. That comparator is already "by grade, then by name", so both are reversed: the grades go from the highest, and Peter comes before Ivan. If you want only the grades in reverse, reversed() goes right after comparing(Student::getGrade), as in the card before this one.Try it
Find the bug
Pick the line with the bug.
The lambda changes total, and that is not allowed:
Main.java:9: error: local variables referenced from a lambda expression must be final or effectively final prices.forEach(p -> total += p); ^A lambda can run later and somewhere else, so it gets a copy of the local variable's value. If the variable could change, the copy and the original would drift apart. So in a lambda Java allows only local variables that do not change after their first value. They are called effectively final: they work even without the word final. This also applies to a change outside the lambda: if somewhere below you write total = 5, the lambda still cannot read total.
For a sum, use a regular loop. In the next lesson you will do it on one line too.
More precisely, if you are curious
Reading such a variable is allowed:
double rate = 0.2;PriceRule discount = price -> price * (1 - rate);System.out.println(discount.apply(100));80.0The fields of an object are not local variables, and a lambda can change them. That is not a way around the rule: if lambdas run in several threads at the same time, a shared field is a source of bugs. Threads are in the module "Real-world programs".
Exercise
Rules as values
Predicate, Function and the rest, add import java.util.function.*;.1. Sorting by several fields. A class Product with a name, a category and a price. Sort the products by category, within a category from the most expensive, and by name when the price is equal. The products are: mouse, accessories, €25; laptop, computers, €1299; keyboard, accessories, €49.90; tablet, computers, €399; headphones, accessories, €49.90.
Accessories | Headphones | €49.90Accessories | Keyboard | €49.90Accessories | Mouse | €25.00Computers | Laptop | €1299.00Computers | Tablet | €399.002. Filtering orders. Add a city to Order. Write a method select(List<Order> orders, Predicate<Order> rule) that returns a new list with only the orders for which the rule is true. Make two rules, sofia and big (at least €50), and print the orders with sofia.and(big), with sofia.negate() and with "Sofia or at least €100". The orders: Maria, Sofia, €75.50; Georgi, Varna, €12.90; Elena, Sofia, €40; Dimitar, Plovdiv, €120.
Sofia, over €50: Maria, Sofia: €75.50Outside Sofia: Georgi, Varna: €12.90 Dimitar, Plovdiv: €120.00Free delivery (Sofia or over €100): Maria, Sofia: €75.50 Elena, Sofia: €40.00 Dimitar, Plovdiv: €120.00Function<Double, Double>: a student discount (−10%), a Black Friday discount (−€20) and "not below zero". Chain them with andThen in two orders: the student discount first and the Friday discount first. Try it with €120 and with €15. Does the order matter?€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00More tasks
- Cleaning a list. From a list of emails, remove the empty ones and the ones without
@with oneremoveIf, then print the rest withforEach(System.out::println). - Students by class. Group students by class (
"10A","10B") in aTreeMap<String, List<String>>withcomputeIfAbsentand print the map withforEach. - Your own interface. Make
@FunctionalInterface interface Validator { boolean check(String value); }and three validators as lambdas: non-empty text, digits only, length up to 10. Try them on a few strings.
Test