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.applydoesn’t declare exceptions, so a lambda that calls code throwingIOExceptionmust 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.
thismeans the enclosing object, not the lambda (unlike anonymous classes, wherethisis 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,FunctionorConsumerparameters to make your own methods flexible. - Keep lambdas short; promote long ones to named methods.