Lambdas & Functional Interfaces

Java · Lesson 23 of 32 6 min read Updated

Sometimes you want to pass behaviour, not just data, to a method: "sort these students by marks", "remove every item that is out of stock", "apply this discount". Before Java 8, you had to wrap that behaviour in a whole class. A lambda expression is a short, unnamed function you can write inline and pass around like a value.

Lambdas are everywhere in modern Java: sorting, collections, streams, threads and event handling all use them. In this lesson you will learn lambda syntax, the functional interfaces that lambdas plug into, and method references, an even shorter form.

From anonymous class to lambda

Here is the same comparator written the old way, with an anonymous class (a class defined and instantiated on the spot), and then as a lambda:

Java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Sorting {
    record Student(String name, String city, int marks) {}

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>(List.of(
            new Student("Meera", "Kochi", 88),
            new Student("Aarav", "Jaipur", 95),
            new Student("Rohan", "Kochi", 72),
            new Student("Isha", "Jaipur", 88)));

        // Old style: an anonymous class
        list.sort(new Comparator<Student>() {
            @Override
            public int compare(Student a, Student b) {
                return a.name().compareTo(b.name());
            }
        });
        System.out.println(list.get(0).name());

        // Lambda: the same comparator in one line
        list.sort((a, b) -> Integer.compare(b.marks(), a.marks()));
        list.forEach(s -> System.out.println(s.name() + " " + s.marks()));

        // Comparator helpers with method references
        list.sort(Comparator.comparing(Student::city)
                            .thenComparing(Student::marks, Comparator.reverseOrder()));
        System.out.println(list);
    }
}
Output
Aarav
Aarav 95
Isha 88
Meera 88
Rohan 72
[Student[name=Aarav, city=Jaipur, marks=95], Student[name=Isha, city=Jaipur, marks=88], Student[name=Meera, city=Kochi, marks=88], Student[name=Rohan, city=Kochi, marks=72]]

A lambda has three parts: the parameters, the arrow ->, and the body.

  • (a, b) -> Integer.compare(b.marks(), a.marks()): two parameters; the body is a single expression whose value is returned automatically.
  • s -> System.out.println(...): one parameter needs no brackets.
  • () -> "Good morning": no parameters needs empty brackets.
  • With several statements, use braces and an explicit return: x -> { int y = x * 2; return y + 1; }.

You rarely write parameter types; the compiler infers them from where the lambda is used. The last sort shows the modern way to build comparators: Comparator.comparing(Student::city) sorts by city, and thenComparing(..., reverseOrder()) breaks ties by marks, highest first.

Functional interfaces

A lambda does not have a type of its own. It always becomes an object of a functional interface: an interface with exactly one abstract method. Comparator is one (its method is compare). The lambda becomes the body of that one method. The package java.util.function provides ready-made functional interfaces for the common shapes of function:

InterfaceMethodTakesReturnsExample use
Predicate<T>testa Tbooleanis this order large?
Function<T, R>applya Tan Rturn a name into its length
Consumer<T>accepta Tnothingprint or save something
Supplier<T>getnothinga Tcreate a default value
UnaryOperator<T>applya Ta Tchange text to upper case
BinaryOperator<T>applytwo Tsa Tadd two numbers
BiFunction<T, U, R>applya T and a Uan Rprice and rate to tax
Java
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class Builtins {
    public static void main(String[] args) {
        Predicate<Integer> isAdult = age -> age >= 18;
        Function<String, Integer> length = s -> s.length();
        Consumer<String> greet = name -> System.out.println("Namaste, " + name);
        Supplier<String> greeting = () -> "Good morning";
        UnaryOperator<String> shout = s -> s.toUpperCase() + "!";
        BinaryOperator<Integer> add = (a, b) -> a + b;
        BiFunction<Integer, Double, Double> gst = (price, rate) -> price * rate;

        System.out.println(isAdult.test(17));
        System.out.println(length.apply("Lucknow"));
        greet.accept("Farhan");
        System.out.println(greeting.get());
        System.out.println(shout.apply("jai ho"));
        System.out.println(add.apply(250, 120));
        System.out.println(gst.apply(1000, 0.18));

        Predicate<String> longName = s -> s.length() > 5;
        Predicate<String> startsWithA = s -> s.startsWith("A");
        Predicate<String> both = longName.and(startsWithA);
        for (String n : List.of("Ananya", "Aarav", "Kabir")) {
            System.out.println(n + ": " + both.test(n) + " / " + both.negate().test(n));
        }

        Function<Integer, Integer> doubleIt = x -> x * 2;
        Function<Integer, Integer> plusTen = x -> x + 10;
        System.out.println(doubleIt.andThen(plusTen).apply(5));
        System.out.println(doubleIt.compose(plusTen).apply(5));
    }
}
Output
false
7
Namaste, Farhan
Good morning
JAI HO!
370
180.0
Ananya: true / false
Aarav: false / true
Kabir: false / true
20
30

Functional interfaces also have default methods for combining functions. and, or and negate combine predicates. andThen runs one function and then another (5 doubled is 10, plus 10 is 20), while compose runs the other one first (5 plus 10 is 15, doubled is 30).

Your own functional interface

When none of the standard interfaces has a good name for your idea, write your own. Add @FunctionalInterface so the compiler checks that it has exactly one abstract method. Methods that accept such an interface let callers plug in any behaviour.

Java
public class Own {
    @FunctionalInterface
    interface DiscountRule {
        double apply(double amount);
    }

    static double checkout(double amount, DiscountRule rule) {
        double after = rule.apply(amount);
        return Math.max(after, 0);
    }

    public static void main(String[] args) {
        DiscountRule festive = amt -> amt * 0.80;
        DiscountRule flat100 = amt -> amt - 100;
        DiscountRule bigSpender = amt -> {
            if (amt >= 5000) {
                return amt - 750;
            }
            return amt;
        };

        System.out.println(checkout(2000, festive));
        System.out.println(checkout(2000, flat100));
        System.out.println(checkout(6000, bigSpender));
        System.out.println(checkout(80, flat100));
    }
}
Output
1600.0
1900.0
5250.0
0.0

checkout does not know or care which discount it receives. New offers can be added without touching it, which is the same idea as polymorphism from Inheritance & Polymorphism, with far less code.

Method references

When a lambda only calls one existing method, a method reference (written with ::) says the same thing more briefly. There are four kinds:

Java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class Refs {
    static boolean isValidPin(String pin) {
        return pin.length() == 6 && pin.chars().allMatch(Character::isDigit);
    }

    public static void main(String[] args) {
        List<String> pins = new ArrayList<>(List.of("124001", "30200", "682001", "11OO01"));

        pins.removeIf(p -> !isValidPin(p));                 // lambda
        System.out.println(pins);

        pins.replaceAll(String::strip);                     // instance method of the element
        pins.forEach(System.out::println);                  // method of a particular object

        Function<String, Integer> parse = Integer::parseInt;  // static method
        System.out.println(parse.apply("411001") + 1);

        Supplier<List<String>> maker = ArrayList::new;      // constructor
        List<String> fresh = maker.get();
        fresh.add("new list");
        System.out.println(fresh);
    }
}
Output
[124001, 682001]
124001
682001
411002
[new list]
  • Integer::parseInt is a static method reference, the same as s -> Integer.parseInt(s).
  • System.out::println calls a method on a particular object (System.out), the same as x -> System.out.println(x).
  • String::strip calls an instance method on the parameter, the same as s -> s.strip().
  • ArrayList::new is a constructor reference, the same as () -> new ArrayList<>().

Use a method reference when it reads more clearly than the lambda; otherwise keep the lambda.

Captured variables must be effectively final

A lambda can use local variables from the surrounding method, but only if they never change after being assigned. Such variables are effectively final. int passMark = 40; Predicate<Integer> passed = m -> m >= passMark; is fine. Trying to change a local variable inside a lambda fails:

Output
Capture.java:5: error: local variables referenced from a lambda expression must be final or effectively final
        List.of(1, 2, 3).forEach(n -> total += n);
                                      ^
1 error

This rule prevents confusing bugs, especially when lambdas run later or on other threads. To add up values, use a loop or a stream, which you will learn in Streams & Optional.

Common mistakes

  • Trying to modify a local variable inside a lambda. It must be effectively final; use a loop or a stream instead.
  • Writing long lambdas. If a lambda grows beyond a few lines, move the code into a named method and use a method reference.
  • Forgetting return in a block body. x -> { x * 2; } does not compile; write x -> { return x * 2; } or simply x -> x * 2.
  • Two abstract methods in a "functional" interface. A lambda needs exactly one; @FunctionalInterface catches this mistake.

Key points

  • A lambda is a short inline function: (params) -> expression or (params) -> { statements }.
  • Lambdas implement functional interfaces: interfaces with exactly one abstract method.
  • java.util.function provides Predicate, Function, Consumer, Supplier, UnaryOperator, BinaryOperator and BiFunction.
  • Method references (Integer::parseInt, System.out::println, String::strip, ArrayList::new) are shorthand for simple lambdas.
  • Comparator.comparing(...).thenComparing(...) builds sort orders clearly.
  • Local variables used inside a lambda must be effectively final.

Exercise

You have a list of products as records Product(String name, String category, int price): Kurta (Clothing, 899), Earphones (Electronics, 1499), Saree (Clothing, 2499), Charger (Electronics, 599), Dupatta (Clothing, 399). Using lambdas and method references only (no for loops), remove products under Rs 500, sort the rest by category and then by price from high to low, and print each product on its own line.

Show solution
Java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Products {
    record Product(String name, String category, int price) {}

    public static void main(String[] args) {
        List<Product> products = new ArrayList<>(List.of(
            new Product("Kurta", "Clothing", 899),
            new Product("Earphones", "Electronics", 1499),
            new Product("Saree", "Clothing", 2499),
            new Product("Charger", "Electronics", 599),
            new Product("Dupatta", "Clothing", 399)));

        products.removeIf(p -> p.price() < 500);
        products.sort(Comparator.comparing(Product::category)
                .thenComparing(Product::price, Comparator.reverseOrder()));
        products.forEach(System.out::println);
    }
}
Output
Product[name=Saree, category=Clothing, price=2499]
Product[name=Kurta, category=Clothing, price=899]
Product[name=Earphones, category=Electronics, price=1499]
Product[name=Charger, category=Electronics, price=599]

removeIf takes a Predicate, sort takes a Comparator built from method references, and forEach takes a Consumer. The list is copied into an ArrayList because List.of cannot be modified.

Quick Quiz

1. Which functional interface fits a lambda that takes a String and returns true or false?

2. What is String::toUpperCase short for?

3. What makes an interface a functional interface?

4. Why does forEach(n -> total += n) fail when total is a local int?

Learn Java with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call