On this page
Description
Java 8 (2014) changed how Java is written more than any release before or since. Its big idea was letting you pass behavior around as easily as data: "sort these orders by total", "keep only the large ones", "run this when the payment completes". Before Java 8, each of those needed a bulky anonymous class. After it, a one-line lambda does the job, and streams, Optional, CompletableFuture and most modern libraries are built around that idea.
Interviewers still ask "what came in Java 8?", and much production code still targets it. This skill covers the language half: lambdas, the interfaces they implement, method references, and default methods in interfaces. The next two skills cover streams, then Optional and java.time.
Here's the path, from easiest to most technical:
- From anonymous class to lambda.
- Functional interfaces: the "shapes" a lambda can fit.
- Method references: an even shorter lambda.
- What a lambda can use from around it, and why checked exceptions get in the way.
- Combining functions, and default methods in interfaces.
From anonymous class to lambda
Here's "sort orders by total" before and after Java 8:
// Before: an anonymous class
orders.sort(new Comparator<Order>() {
@Override
public int compare(Order a, Order b) {
return Long.compare(a.total(), b.total());
}
});
// After: a lambda
orders.sort((a, b) -> Long.compare(a.total(), b.total()));
A lambda is written as (parameters) -> body. The body is either one expression (its value is returned) or a block in braces (with an explicit return if it returns something). Java works out the parameter types from where the lambda is used, so you rarely write them.
Functional interfaces: the shapes a lambda fits
A lambda can be used anywhere Java expects a functional interface: an interface with exactly one abstract method. The lambda becomes the implementation of that method. Java 8 added a standard set in java.util.function, and you'll see these everywhere:
| Interface | Method | Takes → gives | Example |
|---|---|---|---|
Predicate<T> | test | T → boolean | o -> o.total() > 10_000 |
Function<T, R> | apply | T → R | o -> o.id() |
Consumer<T> | accept | T → nothing | o -> log.info("{}", o) |
Supplier<T> | get | nothing → T | () -> new ArrayList<>() |
UnaryOperator<T> | apply | T → T | s -> s.trim() |
BiFunction<T, U, R> | apply | T, U → R | (a, b) -> a + b |
Runnable | run | nothing → nothing | () -> sendEmail() |
A few details:
- Primitive versions such as
IntPredicate,ToIntFunctionandIntUnaryOperatorwork with plainints. APredicate<Integer>boxes every number it tests, which matters in hot loops. @FunctionalInterfaceon your own interface makes the compiler check that it has exactly one abstract method.- Default methods, and methods like
equalsthat come fromObject, don't count toward the "one abstract method".
Method references
When a lambda just calls one existing method, a method reference says the same thing more briefly:
| Kind | Method reference | Same as the lambda |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Method of a particular object | logger::flush | () -> logger.flush() (almost: see below) |
| Method of the argument | String::length | s -> s.length() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
The "almost": logger::flush looks up logger once, when the reference is created, and keeps calling that object. The lambda () -> logger.flush() reads the logger field each time it runs. If the field is later replaced, the two behave differently. And if logger is null, the method reference throws a NullPointerException immediately, while the lambda would throw only when called.
What a lambda can use from around it
A lambda can read the variables around it, with one rule that trips everyone up at first:
int skipped = 0;
orders.forEach(o -> {
if (o.isTest()) skipped++; // doesn't compile: local variables referenced from a lambda
}); // expression must be final or effectively final
A lambda copies the values of the local variables it uses, so Java requires those variables never to change after they're set (they must be effectively final, whether or not you write final). The fix is usually to stop mutating and compute the answer instead:
long skipped = orders.stream().filter(Order::isTest).count();
Two more rules:
- Fields aren't copied. A lambda reads instance fields when it runs, so it sees their current values.
thismeans the surrounding object. Inside a lambda,thisis the object whose method created the lambda. Inside an anonymous class,thisis the anonymous object itself.
Checked exceptions are the other common wall. Consumer.accept and Function.apply don't declare any, so this doesn't compile:
paths.forEach(p -> process(Files.readString(p))); // unreported exception IOException
Either catch inside the lambda and rethrow as an unchecked exception (UncheckedIOException for I/O), or write a small functional interface of your own whose method declares throws IOException, or keep a plain for loop.
Combining functions
Functional interfaces come with methods for combining them:
Function<Integer, Integer> plusOne = x -> x + 1;
Function<Integer, Integer> timesTwo = x -> x * 2;
plusOne.andThen(timesTwo).apply(5); // 12: plusOne first, then timesTwo
plusOne.compose(timesTwo).apply(5); // 11: timesTwo first, then plusOne
Predicate<Order> large = o -> o.total() > 10_000;
Predicate<Order> test = Order::isTest;
Predicate<Order> realLargeOrder = large.and(test.negate());
andThen reads in the order things happen; compose runs its argument first. Mixing them up is a classic bug.
Default methods in interfaces
Before Java 8, adding a method to an interface broke every class that implemented it. Default methods (interface methods with a body) fixed that, and that's how forEach, stream() and getOrDefault were added to Java's existing collection interfaces.
When a class could inherit the same method from more than one place, three rules decide which one runs:
- The class wins. A method from a superclass beats an interface's default.
- The more specific interface wins. If
B extends Aand both have the default,B's is used. - Otherwise it doesn't compile, and the class must override the method itself (it can call one of them with
A.super.greet()).
Interfaces can also have static methods, such as Comparator.naturalOrder(). They belong to the interface only and aren't inherited, so you always call them through the interface name.
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Changing a local variable inside a lambda | doesn't compile | compute the value, e.g. with count() or sum() |
| "Fixing" that with a one-element array | shared mutable state, wrong in parallel | a stream operation that returns the result |
Throwing a checked exception in forEach/map | doesn't compile | wrap in an unchecked exception, or a loop |
| A method reference to a field that's later replaced | keeps calling the old object | a lambda that reads the field each time |
Predicate<Integer> in a hot loop | boxing on every call | IntPredicate |
Mixing up andThen and compose | steps run in the wrong order | andThen = this, then that |
Check yourself
Try to answer before opening each one. Recalling beats re-reading.
1. Why can a lambda read a local variable but not increment it?
Show answer
A lambda captures the variable's value, so Java requires the variable to be effectively final. Otherwise the copy inside the lambda and the variable outside could disagree.
2. What's the difference between logger::flush and () -> logger.flush() when logger is a field that later gets replaced?
Show answer
The method reference looks up logger once, when it's created, and keeps calling that object. The lambda reads the field each time it runs, so it uses the new logger.
3. What do plusOne.andThen(timesTwo).apply(5) and plusOne.compose(timesTwo).apply(5) return?
Show answer
12 and 11. andThen runs plusOne first ((5 + 1) × 2); compose runs timesTwo first (5 × 2 + 1).
4. A class extends Base (which has greet()) and implements Greeter (which has a default greet()). Which one runs?
Show answer
Base.greet(). A method from the class hierarchy always beats an interface's default method.
Key takeaways
- A lambda is a short implementation of a functional interface: one abstract method, such as
Predicate,Function,ConsumerorSupplier. - Method references shorten lambdas that call one method; a reference to an object fixes that object when it's created.
- Lambdas capture effectively final locals, read fields live, and use the enclosing
this. Checked exceptions need wrapping. andThenruns this then that;composeruns that then this.- Default methods let interfaces grow: the class wins, the more specific interface wins, and clashes must be overridden.