Skip to content
wannacode

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.

Order
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());
}
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50

It 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 then return is 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());
}
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50
A lambda is a value: you can put it in a variable and pass it later.

The 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));
Output
40.0
45.0
student 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
  1. The lambda is an object of type PriceRule, and student points to it. Its body does not run yet.
  2. A second rule, a second object. Not a single class is written.
  3. student.apply(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.0.
  4. minusFive.apply(50): the same number, a different rule, 45.0.
More precisely, if you are curious
  • The annotation @FunctionalInterface above the interface makes the compiler check that there is exactly one method. If you add a second abstract method:
@FunctionalInterface
interface PriceRule {
double apply(double price);
String name();
}
Error
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 (default and static) do not count. Neither do the methods from Object: Comparator declares boolean equals(Object obj), but it is still functional.
  • A lambda is not an anonymous class. Inside it, this is 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 a T, returns a boolean. The method is test. It is a condition: "is the order big?".
  • Function<T, R>: takes a T, returns an R. The method is apply. It turns one thing into another: an order into a label.
  • Consumer<T>: takes a T, returns nothing. The method is accept. It does something with it: prints it, saves it.
  • Supplier<T>: takes nothing, returns a T. The method is get. 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());
Output
true
-> MARIA
Guest
The built-in interfaces also have methods that combine rules. Predicates are joined with and, 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> is Function<T, T>, and BinaryOperator<T> is BiFunction<T, T, T>.
  • For the primitive types there are IntPredicate, IntFunction<R>, ToDoubleFunction<T> and others. With them, an int is not boxed into an Integer on every call.
  • f.andThen(g) does f first, then g. f.compose(g) does the opposite: g first, then f.

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));
Answer

See it

Lists and maps take lambdas

Collections have methods that take a rule instead of a loop:
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));
Output
bread
milk
coffee
Plovdiv: [Maria, Elena]
Sofia: [Georgi]
  • removeIf(condition) removes all elements for which the condition is true. That is a Predicate.
  • forEach(action) does the action with each element. That is a Consumer.
  • 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 no if (!byCity.containsKey(...)).
  • The map's forEach takes 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: ::

Often a lambda only passes its parameter to one existing method: 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);
Output
maria
georgi
ELENA
43
[coffee]

This is called a method reference. There are four kinds:

  • a method of a specific object: System.out::println is s -> System.out.println(s);
  • a method of the parameter: String::toUpperCase is s -> s.toUpperCase();
  • a static method: Integer::parseInt is s -> Integer.parseInt(s);
  • a constructor: ArrayList::new is () -> 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));
Output
Ana 6.0
Ivan 5.5
Peter 5.5
Boris 4.75

You 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());
Error
Main.java:26: error: cannot find symbol
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());
^
symbol: method getTotal()
location: variable o of type Object

The 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());
  • comparingInt and comparingDouble do the same without boxing into Integer and Double.
  • Comparator.naturalOrder() and Comparator.reverseOrder() sort by the natural order of the type: numbers by size, text alphabetically.

Try it

What does reversed() reverse?

Here 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()));
Answer

Try it

Find the bug

The program should print the sum of the prices. It does not compile. Which line is the bug on?

Pick the line with the bug.

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));
Output
80.0

The 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

Solve the tasks in your editor. For 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.

Output
Accessories | Headphones | €49.90
Accessories | Keyboard | €49.90
Accessories | Mouse | €25.00
Computers | Laptop | €1299.00
Computers | Tablet | €399.00

2. 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.

Output
Sofia, over €50:
Maria, Sofia: €75.50
Outside Sofia:
Georgi, Varna: €12.90
Dimitar, Plovdiv: €120.00
Free delivery (Sofia or over €100):
Maria, Sofia: €75.50
Elena, Sofia: €40.00
Dimitar, Plovdiv: €120.00
3. Discounts one after another. Make three discounts as Function<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?
Output
€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00
€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00

More tasks

  • Cleaning a list. From a list of emails, remove the empty ones and the ones without @ with one removeIf, then print the rest with forEach(System.out::println).
  • Students by class. Group students by class ("10A", "10B") in a TreeMap<String, List<String>> with computeIfAbsent and print the map with forEach.
  • 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

Check yourself

1. Where can a lambda expression go?
2. Which built-in interface takes an order and returns true or false?
3. What can replace the lambda s -> s.toUpperCase()?
4. Comparator.comparing(Student::getGrade).thenComparing(Student::getName).reversed(). What does reversed() reverse?
5. Why does int total = 0; list.forEach(x -> total += x); not compile?

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