Lesson 13 of 71
Lambdas and Functional Interfaces
A lambda expression (Java 8) is a block of code passed as a value. Its type is always a functional interface: an interface with exactly one abstract method. This lesson covers the standard functional interfaces, lambda and method reference syntax, which variables a lambda may use, how this differs from anonymous classes, composition, and when evaluating a lambda allocates an object.
| Topic | API or rule |
|---|---|
| Functional interface | one abstract method, optional @FunctionalInterface |
| Standard families | Function, Supplier, Consumer, Predicate, BiFunction, UnaryOperator, BinaryOperator |
| Primitive specializations | IntFunction, ToIntFunction, IntPredicate, IntUnaryOperator, IntBinaryOperator, ... |
| Method references | Type::staticMethod, obj::method, Type::instanceMethod, Type::new |
| Capture | local variables must be final or effectively final |
| Composition | andThen, compose, Predicate.and, or, negate |
Example 1: a functional interface and a lambda
An interface with one abstract method can be implemented by a lambda. default and static methods do not count toward the one abstract method, and neither do methods that Object already declares (such as equals). @FunctionalInterface makes the compiler reject the interface if a second abstract method is added.
public class FirstLambda {
@FunctionalInterface
interface PriceRule {
long apply(long cents); // the single abstract method
default PriceRule twice() { return c -> apply(apply(c)); }
boolean equals(Object o); // declared by Object, not counted
}
public static void main(String[] args) {
PriceRule tenPercentOff = cents -> cents - cents / 10;
PriceRule flatFee = cents -> cents + 50;
System.out.println("10% off 1000: " + tenPercentOff.apply(1000));
System.out.println("fee on 1000: " + flatFee.apply(1000));
System.out.println("10% off twice: " + tenPercentOff.twice().apply(1000));
}
}10% off 1000: 900
fee on 1000: 1050
10% off twice: 810The lambda cents -> cents - cents / 10 takes its parameter type (long) and return type from PriceRule.apply. The type the compiler expects at that position is called the target type; the same lambda text can have different types in different contexts.
Example 2: lambda syntax forms
A lambda has a parameter list, an arrow, and a body. The body is an expression (its value is returned) or a block with explicit return. Parameter types may be omitted, written explicitly, or written as var (Java 11), but all parameters use the same style.
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class Syntax {
public static void main(String[] args) {
Supplier<String> none = () -> "no parameters";
Function<String, Integer> one = s -> s.length(); // parentheses optional
BiFunction<Integer, Integer, Integer> typed = (Integer a, Integer b) -> a + b;
BiFunction<Integer, Integer, Integer> withVar = (var a, var b) -> a * b;
Function<Integer, String> block = n -> {
if (n < 0) return "negative";
return "non-negative";
};
System.out.println(none.get());
System.out.println(one.apply("lambda"));
System.out.println(typed.apply(2, 3));
System.out.println(withVar.apply(2, 3));
System.out.println(block.apply(-5));
}
}no parameters
6
5
6
negativeExample 3: the java.util.function families
java.util.function defines 43 interfaces. Most belong to a few families, named by what goes in and what comes out.
| Interface | Method | In, out |
|---|---|---|
Supplier<T> |
get() |
nothing, T |
Consumer<T> |
accept(T) |
T, nothing |
Predicate<T> |
test(T) |
T, boolean |
Function<T, R> |
apply(T) |
T, R |
UnaryOperator<T> |
apply(T) |
T, T (extends Function<T, T>) |
BiFunction<T, U, R> |
apply(T, U) |
two values, R |
BinaryOperator<T> |
apply(T, T) |
two T, T |
The generic interfaces work only with reference types, so an int passes through as a boxed Integer. Primitive specializations avoid that boxing: IntPredicate.test(int), IntUnaryOperator.applyAsInt(int), ToIntFunction<T>.applyAsInt(T), IntFunction<R>.apply(int), and the same for long and double. Boxing is covered in Types and Variables.
import java.util.ArrayList;
import java.util.List;
import java.util.function.*;
public class Families {
public static void main(String[] args) {
Supplier<List<String>> newList = () -> new ArrayList<>();
Consumer<List<String>> addTwo = l -> { l.add("a"); l.add("bb"); };
Predicate<String> longer = s -> s.length() > 1;
Function<String, Integer> len = String::length;
UnaryOperator<String> upper = s -> s.toUpperCase();
BinaryOperator<Integer> max = (a, b) -> a >= b ? a : b;
List<String> list = newList.get();
addTwo.accept(list);
System.out.println(list + " longer[1]=" + longer.test(list.get(1)));
System.out.println("len=" + len.apply("abc") + " upper=" + upper.apply("abc")
+ " max=" + max.apply(3, 7));
IntPredicate even = n -> n % 2 == 0; // no Integer objects
IntUnaryOperator square = n -> n * n;
IntBinaryOperator sum = (a, b) -> a + b;
ToIntFunction<String> length = String::length;
int total = 0;
for (int i = 1; i <= 4; i++) {
if (even.test(i)) total = sum.applyAsInt(total, square.applyAsInt(i));
}
System.out.println("sum of even squares 1..4 = " + total);
System.out.println("length via ToIntFunction = " + length.applyAsInt("hello"));
}
}[a, bb] longer[1]=true
len=3 upper=ABC max=7
sum of even squares 1..4 = 20
length via ToIntFunction = 5Example 4: the four kinds of method references
A method reference is a short form of a lambda that only calls one existing method or constructor.
| Kind | Form | Equivalent lambda |
|---|---|---|
| Static method | Integer::parseInt |
s -> Integer.parseInt(s) |
| Bound instance method | prefix::concat |
s -> prefix.concat(s) |
| Unbound instance method | String::toUpperCase |
s -> s.toUpperCase() |
| Constructor | StringBuilder::new |
s -> new StringBuilder(s) |
In the unbound form, the first parameter of the functional method becomes the receiver. A bound reference evaluates its receiver once, when the reference is created.
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class MethodRefs {
public static void main(String[] args) {
Function<String, Integer> parse = Integer::parseInt; // static
String prefix = "id-";
Function<String, String> addPrefix = prefix::concat; // bound
Function<String, String> upper = String::toUpperCase; // unbound, 1 arg
BiFunction<String, String, Boolean> starts = String::startsWith; // unbound, 2 args
Function<String, StringBuilder> builder = StringBuilder::new; // constructor
Supplier<StringBuilder> empty = StringBuilder::new; // other constructor
System.out.println(parse.apply("42") + 1);
System.out.println(addPrefix.apply("7"));
System.out.println(upper.apply("ref"));
System.out.println(starts.apply("lambda", "lam"));
System.out.println(builder.apply("abc").reverse());
System.out.println("empty length " + empty.get().length());
}
}43
id-7
REF
true
cba
empty length 0StringBuilder::new matches two different constructors because the target type decides which overload is used: Function<String, StringBuilder> selects StringBuilder(String), Supplier<StringBuilder> selects StringBuilder().
Example 5: capture and effectively final
A lambda may read local variables of the enclosing method only if they are final or effectively final (never assigned after initialization). The lambda receives a copy of the value, so the rule prevents the copy and the variable from disagreeing. Fields and array elements have no such rule, because the lambda reads them through a reference.
import java.util.function.IntSupplier;
import java.util.function.Supplier;
public class Capture {
private int counter; // a field: lambdas may change it
void run() {
int base = 100; // effectively final
IntSupplier plusBase = () -> base + 1;
// base++; // would make "base" not effectively final:
// compile error in the lambda above
Supplier<Integer> next = () -> ++counter; // captures this, not a copy of counter
next.get();
next.get();
int[] box = {0}; // the array reference is effectively final
Runnable inc = () -> box[0]++;
inc.run();
inc.run();
inc.run();
System.out.println("plusBase=" + plusBase.getAsInt());
System.out.println("counter=" + counter + " box[0]=" + box[0]);
}
public static void main(String[] args) {
new Capture().run();
}
}plusBase=101
counter=2 box[0]=3The one-element array is a known workaround, but it is not thread safe: two threads running inc can lose updates. Atomics and CAS covers the thread-safe counter.
The next program shows the compile error when a captured variable is reassigned.
import java.util.function.IntSupplier;
public class NotEffectivelyFinal {
public static void main(String[] args) {
int base = 100;
base = 200;
IntSupplier s = () -> base; // error: local variables referenced from a lambda
// expression must be final or effectively final
System.out.println(s.getAsInt());
}
}Example 6: this in lambdas vs anonymous classes
An anonymous class is a new class, so this inside it refers to the anonymous object, and its own members shadow the enclosing ones. A lambda is not a new scope for names: this and toString() refer to the enclosing instance, and a lambda parameter may not reuse the name of a local variable.
public class ThisDemo {
private final String name = "outer";
@Override public String toString() { return "ThisDemo(" + name + ")"; }
void show() {
Runnable anonymous = new Runnable() {
@Override public String toString() { return "anonymous object"; }
@Override public void run() { System.out.println("anonymous this: " + this); }
};
Runnable lambda = () -> System.out.println("lambda this: " + this);
anonymous.run();
lambda.run();
System.out.println("lambda is a ThisDemo: " + ((Object) lambda instanceof ThisDemo));
}
public static void main(String[] args) {
new ThisDemo().show();
}
}anonymous this: anonymous object
lambda this: ThisDemo(outer)
lambda is a ThisDemo: falseThe lambda object itself is not a ThisDemo; only the name this inside its body resolves to the enclosing object. A lambda that uses this (or an instance field) captures the enclosing instance and keeps it reachable for as long as the lambda lives.
Example 7: composition
Function.andThen(g) applies this function, then g. Function.compose(g) applies g first. Predicate has and, or, negate, and the static Predicate.not (Java 11). Consumer.andThen runs two consumers in order. Each call returns a new functional object, so a rule set can be built from small parts.
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class Compose {
public static void main(String[] args) {
Function<Integer, Integer> plus2 = x -> x + 2;
Function<Integer, Integer> times3 = x -> x * 3;
System.out.println("plus2.andThen(times3)(1) = " + plus2.andThen(times3).apply(1));
System.out.println("plus2.compose(times3)(1) = " + plus2.compose(times3).apply(1));
Predicate<String> nonEmpty = Predicate.not(String::isEmpty);
Predicate<String> shortWord = s -> s.length() <= 3;
Predicate<String> accepted = nonEmpty.and(shortWord.negate()).or(s -> s.equals("ok"));
StringBuilder log = new StringBuilder();
Consumer<String> print = s -> log.append(s);
Consumer<String> sep = s -> log.append(';');
Consumer<String> both = print.andThen(sep);
for (String s : List.of("", "ok", "abc", "trade", "order")) {
if (accepted.test(s)) both.accept(s);
}
System.out.println("accepted: " + log);
}
}plus2.andThen(times3)(1) = 9
plus2.compose(times3)(1) = 5
accepted: ok;trade;order;Example 8: capturing vs non-capturing lambdas and allocation
The compiler turns a lambda body into a private method and emits an invokedynamic instruction that produces the functional object at run time; the instruction is covered in Bytecode and Class Loading. The language specification allows an implementation to reuse lambda objects. On HotSpot a non-capturing lambda (one that uses no local variables and no this) evaluates to the same object every time, so it does not allocate after the first use. A capturing lambda stores the captured values in a new object on each evaluation.
import java.util.function.IntSupplier;
import java.util.function.IntUnaryOperator;
public class Allocation {
static IntUnaryOperator nonCapturing() { return x -> x + 1; }
static IntSupplier capturing(int value) { return () -> value; }
public static void main(String[] args) {
IntUnaryOperator a = nonCapturing();
IntUnaryOperator b = nonCapturing();
IntSupplier c = capturing(5);
IntSupplier d = capturing(5);
System.out.println("non-capturing same object: " + (a == b));
System.out.println("capturing same object: " + (c == d));
System.out.println("results: " + a.applyAsInt(1) + " " + c.getAsInt() + " " + d.getAsInt());
System.out.println("lambda class is synthetic: " + a.getClass().isSynthetic());
System.out.println("lambda class is hidden: " + a.getClass().isHidden());
}
}non-capturing same object: true
capturing same object: false
results: 2 5 5
lambda class is synthetic: true
lambda class is hidden: trueThe lambda classes are hidden classes (Java 15), generated at run time and not visible by name to other code. Programs must not depend on identity of lambda objects: == between them is unspecified by the language. On a hot path, a capturing lambda inside a loop can allocate on each iteration; the JIT may remove that allocation (escape analysis is covered in a later section), but code that must not allocate keeps its lambdas non-capturing or stores them in fields.
Interview points
- A functional interface has exactly one abstract method;
default,static, andObjectmethods do not count.@FunctionalInterfacemakes the rule a compile check. - A lambda has no type of its own; the target type decides it, which is why
StringBuilder::newcan match different constructors. - Four method reference kinds: static, bound instance (
obj::m), unbound instance (Type::m, first argument is the receiver), constructor (Type::new). - Captured locals must be final or effectively final because the lambda copies the value.
- In a lambda,
thisis the enclosing instance; in an anonymous class, it is the anonymous object. - Use
IntPredicate,ToIntFunction,IntUnaryOperatorand the other primitive specializations to avoid boxing. f.andThen(g)isg(f(x));f.compose(g)isf(g(x)).- Lambdas compile to a private method plus
invokedynamic; non-capturing lambdas are reused on HotSpot, capturing lambdas allocate per evaluation.
Summary
- Lambdas and method references are values whose type is a functional interface.
java.util.functioncovers suppliers, consumers, predicates, functions, and operators, with primitive versions forint,long, anddouble.- A lambda body shares the enclosing scope for names, including
this. - Composition methods return new functional objects built from existing ones.
- Lambda classes are hidden classes generated at run time; object identity of lambdas is not specified.
