What you'll learn

  • Lambda syntax, from one-liners to block bodies
  • Replacing anonymous classes
  • Comparators and sorting with lambdas
  • Passing behaviour into your own methods
  • Common lambda pitfalls

A lambda expression is a short, unnamed function you can store in a variable or pass to a method. Before Java 8, passing behaviour meant writing an anonymous class — five lines of ceremony for one line of logic. Lambdas reduce that to the logic itself.

From anonymous class to lambda

The old way to sort orders by amount:

Comparator<Order> byAmount = new Comparator<>() {
    @Override
    public int compare(Order a, Order b) {
        return Double.compare(a.amount(), b.amount());
    }
};

The same thing as a lambda:

Comparator<Order> byAmount = (a, b) -> Double.compare(a.amount(), b.amount());

And with a comparator factory and a method reference:

Comparator<Order> byAmount = Comparator.comparingDouble(Order::amount);

All three do exactly the same thing. The lambda works because Comparator is a functional interface: Java knows the lambda is the implementation of its single method, compare.

Lambda syntax, all the forms

Runnable hello = () -> System.out.println("No parameters");

Function<Integer, Integer> square = x -> x * x;          // one parameter: no parentheses needed

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;

BiFunction<Integer, Integer, Integer> addTyped = (Integer x, Integer y) -> x + y;   // explicit types (rarely needed)

Function<String, String> describe = s -> {               // block body: braces and return
    String trimmed = s.trim();
    return trimmed.isEmpty() ? "(empty)" : trimmed;
};

Rules of thumb: parameters on the left of ->, the body on the right. A single expression returns its value automatically; a block needs return.

Sorting with lambdas

orders.sort(Comparator.comparingDouble(Order::amount).reversed());

orders.sort(Comparator.comparing(Order::customer)
                      .thenComparing(Order::amount, Comparator.reverseOrder()));

thenComparing adds tie-breakers — “by customer, then biggest amount first” — without a single if.

Passing behaviour into your own methods

This is the real superpower. Write the loop once, and let callers decide what to do:

static List<Order> filter(List<Order> orders, Predicate<Order> rule) {
    List<Order> result = new ArrayList<>();
    for (Order o : orders) {
        if (rule.test(o)) result.add(o);
    }
    return result;
}

List<Order> big  = filter(orders, o -> o.amount() > 200);
List<Order> acme = filter(orders, o -> o.customer().equals("Acme"));

No BigOrderFilter class, no AcmeOrderFilter class — just the rule. (Streams provide filter built in, as you’ll see in Streams API: the basics.)

Lambdas in the Collections API

orders.forEach(o -> System.out.println(o.id()));
totals.merge(o.customer(), o.amount(), Double::sum);
ids.replaceAll(String::toUpperCase);
numbers.removeIf(n -> n < 0);
cache.computeIfAbsent(key, k -> loadFromDatabase(k));

computeIfAbsent is especially handy: “give me the value for this key, and if there isn’t one, create it with this function”.

Pitfalls

  • Local variables must be effectively final. A lambda can read a local variable only if it’s never reassigned. This is the subject of Closures in Java.
  • Checked exceptions. Function.apply doesn’t declare exceptions, so a lambda that calls code throwing IOException must catch it inside the lambda.
  • Readability. If a lambda grows beyond two or three lines, move it into a named method and use a method reference. Names explain intent.
  • this means the enclosing object, not the lambda (unlike anonymous classes, where this is the anonymous object).

The full example

// Lambdas.java — lambda syntax, sorting with comparators, lambdas as arguments.
import java.util.*;
import java.util.function.*;

public class Lambdas {

    record Order(String id, String customer, double amount) { }

    // A method that accepts behaviour as a parameter
    static List<Order> filter(List<Order> orders, Predicate<Order> rule) {
        List<Order> result = new ArrayList<>();
        for (Order o : orders) {
            if (rule.test(o)) result.add(o);
        }
        return result;
    }

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("PO-1", "Acme", 250.0),
                new Order("PO-2", "Globex", 90.0),
                new Order("PO-3", "Acme", 1200.0));

        // 1. The old way: an anonymous class
        Comparator<Order> byAmountOld = new Comparator<>() {
            @Override
            public int compare(Order a, Order b) {
                return Double.compare(a.amount(), b.amount());
            }
        };

        // 2. The same thing as a lambda
        Comparator<Order> byAmount = (a, b) -> Double.compare(a.amount(), b.amount());

        // 3. Even shorter with a comparator factory
        Comparator<Order> byAmountShort = Comparator.comparingDouble(Order::amount);

        List<Order> sorted = new ArrayList<>(orders);
        sorted.sort(byAmountShort.reversed());
        sorted.forEach(o -> System.out.println(o.id() + " " + o.amount()));
        System.out.println(byAmountOld.compare(orders.get(0), orders.get(1)) == byAmount.compare(orders.get(0), orders.get(1)));

        // Lambda syntax variations
        Runnable hello = () -> System.out.println("No parameters");
        Function<Integer, Integer> square = x -> x * x;                 // one parameter
        BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;     // two parameters
        Function<String, String> describe = s -> {                       // block body
            String trimmed = s.trim();
            return trimmed.isEmpty() ? "(empty)" : trimmed;
        };
        hello.run();
        System.out.println(square.apply(7) + " " + add.apply(2, 3) + " " + describe.apply("   "));

        // Passing behaviour into our own method
        List<Order> big = filter(orders, o -> o.amount() > 200);
        List<Order> acme = filter(orders, o -> o.customer().equals("Acme"));
        System.out.println("Big: " + big.size() + ", Acme: " + acme.size());

        // Collections methods that take lambdas
        Map<String, Double> totals = new TreeMap<>();
        orders.forEach(o -> totals.merge(o.customer(), o.amount(), Double::sum));
        System.out.println(totals);                                      // {Acme=1450.0, Globex=90.0}

        List<String> ids = new ArrayList<>(List.of("po-3", "po-1"));
        ids.replaceAll(String::toUpperCase);
        System.out.println(ids);                                         // [PO-3, PO-1]
    }
}

Try it yourself

Sort a list of order IDs like "PO-10", "PO-2", "PO-1" by their number, not alphabetically (alphabetically, PO-10 comes before PO-2).

Show a solution
List<String> ids = new ArrayList<>(List.of("PO-10", "PO-2", "PO-1"));
ids.sort(Comparator.comparingInt(id -> Integer.parseInt(id.substring(3))));
System.out.println(ids);   // [PO-1, PO-2, PO-10]

comparingInt takes a function that extracts the number to compare by.

Key takeaways

  • A lambda is a compact implementation of a functional interface: (params) -> body.
  • Lambdas replace anonymous classes and make sorting, filtering and callbacks one-liners.
  • Accept Predicate, Function or Consumer parameters to make your own methods flexible.
  • Keep lambdas short; promote long ones to named methods.

Example files