Lambdas and functional interfaces
Every time this course has used Comparator.comparing(Sub::name) or
computeIfAbsent(k, key -> new ArrayList<>()), it has been writing a small
function and passing it to a method. This lesson explains what those actually
are, because they underpin the whole of the rest of this module.
The one idea: in Java, a function you pass around is an object implementing an interface with a single method. Lambdas are shorthand for writing that object.
What a lambda replaces
Before Java 8, passing behaviour meant an anonymous class:
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
Five lines of ceremony around one expression. The lambda:
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
They compile to different things — a lambda does not create a new class file — but for your purposes they mean the same and the lambda is what you write.
Functional interfaces
A lambda can be assigned to any interface with exactly one abstract method. That is a functional interface, and it can be one of your own:
@FunctionalInterface
interface Discount {
long applyTo(long paise);
}
Discount student = paise -> paise * 90 / 100;
System.out.println(student.applyTo(8_235));
The @FunctionalInterface annotation is optional and worth adding: it makes the
compiler reject a second abstract method, so nobody breaks every lambda in the
codebase by adding one. Default and static methods do not count.
Most of the time you will not declare your own. java.util.function has the
shapes already, and using the standard ones means your method works with every
library that expects them.
The ones you need to know
| Interface | Method | Shape | Example |
|---|---|---|---|
Predicate<T> |
test(T) |
T to boolean | s -> s.length() > 5 |
Function<T,R> |
apply(T) |
T to R | String::length |
Supplier<T> |
get() |
nothing to T | () -> "Namaste" |
Consumer<T> |
accept(T) |
T to nothing | s -> System.out.println(s) |
UnaryOperator<T> |
apply(T) |
T to T | s -> s.toUpperCase() |
BiFunction<T,U,R> |
apply(T,U) |
two in, R out | (s, n) -> s.repeat(n) |
BinaryOperator<T> |
apply(T,T) |
two T, one T | Integer::sum |
BiConsumer<T,U> |
accept(T,U) |
two in, nothing out | (k, v) -> print(k, v) |
Runnable |
run() |
nothing to nothing | () -> doIt() |
And the primitive versions that avoid boxing — IntPredicate,
ToIntFunction<T>, IntUnaryOperator, ToLongFunction<T> and friends. You will
meet ToLongFunction the moment you total money in paise.
Learn to read the shape, not the name. "Takes one thing, returns a boolean"
is a Predicate. "Takes one thing, returns another" is a Function. That is
most of the package.
Syntax variations
(String s) -> s.length() // explicit type, rarely needed
(s) -> s.length() // inferred
s -> s.length() // one parameter, no brackets
(a, b) -> a + b // two parameters, brackets required
() -> "Namaste" // no parameters
s -> { int n = s.length(); return n * 2; } // block body needs return
A block body needs return; an expression body does not. Mixing them up produces
missing return statement or incompatible types: bad return type in lambda
expression.
Composing them
The functional interfaces come with combinators, which is where they become genuinely pleasant:
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
isLong.negate().test("Amit"); // true
startsWithA.and(isLong).test("Amit"); // false
startsWithA.or(isLong).test("Amit"); // true
Function<String, Integer> length = String::length;
Function<String, Integer> doubled = length.andThen(n -> n * 2);
doubled.apply("Priya"); // 10
| Combinator | On | Does |
|---|---|---|
and / or / negate |
Predicate |
Boolean logic |
andThen |
Function, Consumer |
Run this, then that |
compose |
Function |
Run that, then this |
Predicate.not(p) |
static | Same as negate, reads better in a stream |
Predicate.not(String::isBlank) in a filter is much clearer than
s -> !s.isBlank(), and you will use it constantly.
Method references
When a lambda does nothing but call an existing method, name the method instead. There are four kinds:
Function<String, Integer> parse = Integer::parseInt; // static method
Function<String, String> upper = String::toUpperCase; // instance method of the parameter
Function<String, String> greet = prefix::concat; // instance method of a specific object
Supplier<ArrayList<String>> maker = ArrayList::new; // constructor
The second is the one that confuses people. String::toUpperCase takes no
argument as a method, yet it becomes a Function<String, String> — because the
receiver becomes the parameter. s -> s.toUpperCase() and
String::toUpperCase are the same thing.
Use a method reference when the lambda adds nothing. s -> s.length()
becomes String::length. But s -> s.length() > 5 stays a lambda, because it
does more than call a method.
Effectively final
A lambda can read local variables from the enclosing method, but only if they never change:
int limit = 5;
Predicate<String> under = s -> s.length() < limit;
Add limit = 6; anywhere in the method and the lambda stops compiling:
local variables referenced from a lambda expression must be final or effectively
final.
The reason: the lambda may outlive the method — stored in a field, passed to another thread, run later — and Java captures the value, not the variable. Allowing it to change would mean two versions of the truth.
Fields are different. A lambda can read and write instance fields freely, because
it captures this, not a copy.
The workaround people find, and should not use:
int[] counter = {0};
list.forEach(s -> counter[0]++);
That compiles — the array reference is effectively final even though its contents
change — and it works. It is also a sign you are using a functional construct to
do an imperative job. Count with stream().count() or a collector instead.
this inside a lambda
In an anonymous class, this refers to the anonymous instance. In a lambda, it
refers to the enclosing object. The lambda is not a new scope in that sense —
it does not have its own this, and cannot shadow a variable from the enclosing
method either.
That is nearly always what you want, and it is one more reason to prefer lambdas to anonymous classes.
What lambdas are not for
- Long bodies. A lambda over about three lines should be a method, with a name that says what it does. Then pass a method reference.
- Side effects inside a stream. Adding to a list from inside
maporfilterworks and is a bad idea for reasons the next lesson covers. - Replacing every loop. A
forloop with an index, an earlybreak, or two things happening at once is usually clearer as a loop. The final lesson of this module is explicit about when not to convert. - Checked exceptions. None of the standard functional interfaces declare
them, so a lambda that throws
IOExceptionwill not compile againstFunction. Module 7 comes back to this; it is a genuine rough edge.
Check your work
What makes an interface functional? Exactly one abstract method. Default and
static methods do not count. @FunctionalInterface asks the compiler to enforce
it.
Which interface takes one value and returns a boolean? Predicate<T>, with
test.
What is the difference between s -> s.toUpperCase() and
String::toUpperCase? Nothing — the receiver of an unbound instance method
reference becomes the parameter.
Why must a captured local variable be effectively final? The lambda may outlive the method, so Java captures the value rather than the variable. A changing variable would mean two versions of the truth.
Why is capturing a one-element array to count things a smell? It works
because the array reference never changes, but it is imperative state inside a
functional construct. Use count() or a collector.
What does this refer to inside a lambda? The enclosing object — unlike an
anonymous class, where it refers to the anonymous instance.
Name a case where a lambda is the wrong choice. A body longer than about
three lines; a loop needing an index or an early break; anything throwing a
checked exception.
Practice 2, the four method references. Integer::parseInt is a static
method. String::toUpperCase is an unbound instance method — the receiver
becomes the parameter. prefix::concat is bound to one specific object.
ArrayList::new is a constructor reference and fits Supplier. If you could not
make the second one type-check, the clue is that it is a Function<String, String> and not a Supplier<String>.
Practice 4, the effectively final error.
error: local variables referenced from a lambda expression must be final or effectively final
Moving the variable into a field makes it compile, because the lambda then
captures this and reads the field each time it runs — which also means the
behaviour changes if the field changes later. That difference is the whole reason
for the rule.
Practice
-
Convert three anonymous classes. Write a
Comparator, aRunnableand aPredicateas anonymous classes, then rewrite each as a lambda. Count the lines. -
Use all four kinds of method reference. A static one, an unbound instance one, a bound one and a constructor one. Assign each to the right functional interface.
-
Compose predicates. Build
isLong,startsWithAand combinations withand,orandnegate. Then rewrite one filter usingPredicate.not(...)and decide which reads better. -
Break effectively final. Capture a local variable in a lambda, then assign to it afterwards. Read the error. Then make it a field and confirm it compiles — and work out what changed about when the value is read.
-
Write your own functional interface.
Discountwithlong applyTo(long paise). Implement student, festival and none as lambdas, put them in aMap<String, Discount>and apply one by name. Then add a second abstract method and read what@FunctionalInterfacesays. -
Harder — a validation pipeline. Write
Predicate<Sub>rules for a valid subscriber: non-blank name, six-digit pincode, tiffins between 0 and 62. Put them in aList<Predicate<Sub>>with a message each, and write a method that returns every rule a given subscriber fails. Then combine them all withreduce(Predicate::and)into a single predicate and note what you lose — you get one yes-or-no answer and no longer know which rule failed. That trade comes back in the capstone.
Next: streams, which are these functions applied to a whole collection at once.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship