Lambdas & Functional Interfaces
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:
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);
}
}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:
| Interface | Method | Takes | Returns | Example use |
|---|---|---|---|---|
Predicate<T> | test | a T | boolean | is this order large? |
Function<T, R> | apply | a T | an R | turn a name into its length |
Consumer<T> | accept | a T | nothing | print or save something |
Supplier<T> | get | nothing | a T | create a default value |
UnaryOperator<T> | apply | a T | a T | change text to upper case |
BinaryOperator<T> | apply | two Ts | a T | add two numbers |
BiFunction<T, U, R> | apply | a T and a U | an R | price and rate to tax |
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));
}
}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.
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));
}
}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:
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);
}
}[124001, 682001] 124001 682001 411002 [new list]
Integer::parseIntis a static method reference, the same ass -> Integer.parseInt(s).System.out::printlncalls a method on a particular object (System.out), the same asx -> System.out.println(x).String::stripcalls an instance method on the parameter, the same ass -> s.strip().ArrayList::newis 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:
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 errorThis 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
returnin a block body.x -> { x * 2; }does not compile; writex -> { return x * 2; }or simplyx -> x * 2. - Two abstract methods in a "functional" interface. A lambda needs exactly one;
@FunctionalInterfacecatches this mistake.
Key points
- A lambda is a short inline function:
(params) -> expressionor(params) -> { statements }. - Lambdas implement functional interfaces: interfaces with exactly one abstract method.
java.util.functionprovidesPredicate,Function,Consumer,Supplier,UnaryOperator,BinaryOperatorandBiFunction.- 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
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);
}
}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?
A Predicate takes one value and returns a boolean.
2. What is String::toUpperCase short for?
A reference to an instance method of the parameter calls that method on the lambda's argument.
3. What makes an interface a functional interface?
A lambda can implement an interface only when there is exactly one abstract method to fill in.
4. Why does forEach(n -> total += n) fail when total is a local int?
A lambda may read local variables only if they never change.
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
