A lambda is an anonymous function — a block of behavior you can pass around as a value, without writing a named method or a class. It's compact syntax for implementing a functional interface (an interface with a single abstract method). Introduced in Java 8, it brought functional-style programming to the language.
// verbose anonymous class
Runnable r1 = new Runnable() {
public void run() { System.out.println("hi"); }
};
// same thing as a lambda
Runnable r2 = () -> System.out.println("hi");
The compiler matches the lambda to the interface's single method, so r2's body
becomes run(). Lambdas make code that takes behavior as a parameter
(callbacks, comparators, stream operations) far less verbose.
A lambda is parameters -> body. The parts can be written several ways
depending on how many parameters there are and whether the body is one
expression or a block.
() -> 42 // no params, expression body
x -> x * 2 // one param, parens optional
(x) -> x * 2 // one param, explicit parens
(x, y) -> x + y // multiple params need parens
(int x, int y) -> x + y // explicit types (rare; usually inferred)
(x, y) -> { return x + y; } // block body needs { } and return
Key rules: single-expression bodies have no braces and no return (the value
is implicit); a block body needs braces and an explicit return if it
returns a value. Parameter types are usually inferred, so you omit them.
A functional interface is an interface with exactly one abstract method
(a SAM — Single Abstract Method). That single method is the target a lambda
or method reference implements. It can still have any number of default and
static methods — those don't count toward the one-abstract-method rule.
@FunctionalInterface
interface Transformer {
String apply(String s); // the one abstract method
default String twice(String s) { return apply(apply(s)); } // allowed
}
Transformer upper = s -> s.toUpperCase();
Examples in the JDK: Runnable, Callable, Comparator, and everything in
java.util.function. Because there's only one abstract method, the compiler
knows unambiguously which method your lambda body implements.
@FunctionalInterface is an optional marker that tells the compiler "this
interface is meant to have exactly one abstract method." If you accidentally add
a second abstract method, the build fails — it's a safety net, not a
requirement.
@FunctionalInterface
interface Calc {
int op(int a, int b);
// int other(int x); // would cause a COMPILE ERROR
}
The annotation has no runtime effect; a lambda works on any interface with one
abstract method whether or not it's annotated. But adding it documents intent and
protects callers who rely on lambda compatibility from a careless edit.
Note methods inherited from Object (like equals) don't count as abstract.
java.util.function provides the general-purpose functional interfaces so
you rarely need to write your own. The four families are Function, Consumer,
Supplier, and Predicate, plus operator/two-arg variants:
| Interface | Abstract method | Takes / returns |
|---|---|---|
Function<T,R> |
R apply(T t) |
T -> R |
BiFunction<T,U,R> |
R apply(T,U) |
(T,U) -> R |
Supplier<T> |
T get() |
() -> T |
Consumer<T> |
void accept(T t) |
T -> void |
BiConsumer<T,U> |
void accept(T,U) |
(T,U) -> void |
Predicate<T> |
boolean test(T t) |
T -> boolean |
BiPredicate<T,U> |
boolean test(T,U) |
(T,U) -> boolean |
UnaryOperator<T> |
T apply(T t) |
T -> T |
BinaryOperator<T> |
T apply(T,T) |
(T,T) -> T |
UnaryOperator and BinaryOperator are just Function/BiFunction specialized
to a single type — handy for things like String::trim or Integer::sum.
Function<T,R> represents a transformation: it takes one argument of type
T and returns a result of type R via apply. It's the workhorse behind
Stream.map.
Function<String, Integer> length = s -> s.length();
length.apply("hello"); // 5
// used in a stream
List<Integer> lens = names.stream()
.map(String::length)
.toList();
When input and output are the same type, prefer UnaryOperator<T> for
clarity. For two inputs use BiFunction<T,U,R>.
They're mirror images. A Supplier<T> takes no input and produces a
value (T get()) — a factory or lazy source. A Consumer<T> takes a
value and returns nothing (void accept(T)) — a side effect like printing
or storing.
Supplier<Double> rng = () -> Math.random(); // produces
rng.get();
Consumer<String> printer = s -> System.out.println(s); // consumes
printer.accept("hi");
Suppliers power lazy evaluation (Optional.orElseGet, Logger message
suppliers); consumers power forEach and Optional.ifPresent. Think of it as
output-only vs input-only.
Predicate<T> takes a T and returns a boolean via test — it represents a
condition. It's what Stream.filter expects.
Predicate<Integer> isEven = n -> n % 2 == 0;
isEven.test(4); // true
List<Integer> evens = nums.stream()
.filter(isEven)
.toList();
Predicates compose with and, or, and negate (covered separately), letting
you build complex conditions from simple ones. For two arguments use
BiPredicate<T,U>.
Both chain two functions, but in opposite order. f.andThen(g) runs f
first, then g on f's result. f.compose(g) runs g first, then f — matching
the mathematical f(g(x)).
Function<Integer,Integer> times2 = x -> x * 2;
Function<Integer,Integer> plus3 = x -> x + 3;
times2.andThen(plus3).apply(5); // (5*2)+3 = 13
times2.compose(plus3).apply(5); // (5+3)*2 = 16
Mnemonic: andThen reads left-to-right (do this, and then that);
compose reads inside-out. Both return a new Function, leaving the
originals untouched.
Predicate has three default composition methods that return a new predicate:
and (both must pass), or (either passes), and negate (flips the
result). They let you build readable compound conditions.
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> shortStr = s -> s.length() < 10;
Predicate<String> valid = notBlank.and(shortStr);
Predicate<String> blank = notBlank.negate();
Predicate<String> either = notBlank.or(shortStr);
valid.test("hello"); // true
and short-circuits like && (skips the second test if the first fails). There's
also a static Predicate.not(...) (Java 11+) so you can negate a method
reference: filter(Predicate.not(String::isBlank)).
A method reference (::) is shorthand for a lambda that just calls an existing
method. When a lambda does nothing but forward its arguments to one method, a
method reference is cleaner. There are four kinds:
| Kind | Syntax | Lambda equivalent |
|---|---|---|
| Static | Integer::parseInt |
s -> Integer.parseInt(s) |
| Bound instance | out::println (specific object) |
s -> out.println(s) |
| Unbound instance | String::toUpperCase (type) |
s -> s.toUpperCase() |
| Constructor | ArrayList::new |
() -> new ArrayList<>() |
Function<String,Integer> parse = Integer::parseInt; // static
Consumer<String> print = System.out::println; // bound
Function<String,String> up = String::toUpperCase; // unbound
Supplier<List<String>> make = ArrayList::new; // constructor
The compiler infers which method from the target functional interface's signature.
Both reference an instance method, but differ in who supplies the receiver (the object the method runs on). A bound reference captures a specific object now; an unbound reference uses the first argument as the receiver.
String prefix = "Hello, ";
// BOUND — receiver is the captured `prefix` object
Function<String,String> greet = prefix::concat;
greet.apply("Ada"); // "Hello, Ada"
// UNBOUND — receiver is the lambda's first arg
Function<String,Integer> len = String::length;
len.apply("Ada"); // 3 (called as "Ada".length())
Tell them apart by the left side of ::: a value/variable is bound, a
type name is unbound. For an unbound reference the functional interface has
one extra leading parameter for the receiver.
A lambda may capture local variables from its enclosing scope, but only ones
that are final or effectively final — meaning assigned exactly once and never
reassigned afterward (even without the final keyword). Reassigning a captured
variable is a compile error.
int factor = 3; // effectively final — never reassigned
Function<Integer,Integer> f = x -> x * factor; // OK
int counter = 0;
Runnable bad = () -> counter++; // COMPILE ERROR — counter is reassigned
Instance and static fields are not subject to this — only local variables. The restriction exists because the lambda captures a copy of the value, so allowing reassignment would create confusing divergence between the two copies.
Because the lambda may outlive the method that created it (it can be stored and run later, possibly on another thread). Java captures locals by value, so letting you mutate the original would be ambiguous and unsafe. The fix is to mutate state held inside an object the variable points to, rather than the variable itself.
// won't compile: total is a reassigned local
// int total = 0; list.forEach(n -> total += n);
int[] total = {0}; // array holds mutable state
list.forEach(n -> total[0] += n);
AtomicInteger sum = new AtomicInteger(); // thread-safe alternative
list.forEach(sum::addAndGet);
That said, the idiomatic answer is to avoid mutation entirely — use a stream
reduction (mapToInt(...).sum()) instead of accumulating into a captured variable.
They look similar but differ in important ways:
| Aspect | Lambda | Anonymous class |
|---|---|---|
this |
refers to the enclosing instance | refers to the anonymous object |
| New scope | no new scope (shares the method's) | introduces its own scope |
| Shadowing | can't shadow enclosing locals | can declare same-named locals |
| Compilation | invokedynamic (no extra .class) |
generates a separate .class file |
| State | only the SAM (one method) | can have fields and multiple methods |
Runnable lambda = () -> System.out.println(this); // enclosing `this`
Runnable anon = new Runnable() {
public void run() { System.out.println(this); } // the Runnable itself
};
The this binding is the classic interview gotcha: inside a lambda, this is the
surrounding object, not the functional-interface instance.
No. Unlike anonymous classes (which generate a Foo$1.class at compile time),
lambdas are compiled to an invokedynamic bytecode instruction plus a private
synthetic method holding the body. At runtime the JVM's LambdaMetafactory
builds the implementation lazily on first use.
anonymous class -> extra .class file generated at compile time
lambda -> invokedynamic; implementation linked at runtime
Benefits: no class-file explosion, smaller jars, and the JVM can optimize the strategy over time. A stateless lambda that captures nothing may even be reused as a singleton. This is why lambdas are generally lighter than anonymous classes.
A lambda has no inherent type — its type is determined by the context it appears in, called the target type. The same lambda can implement different functional interfaces depending on the variable, parameter, or return type it's assigned to.
Runnable r = () -> doWork(); // target type Runnable
Callable<Void> c = () -> { doWork(); return null; };
Comparator<String> byLen = (a, b) -> a.length() - b.length();
// the SAME shape could fit any (String,String)->int interface
The compiler reads the target type, finds its single abstract method, and checks
the lambda's parameters and return against that signature. This is why a lambda
can't be assigned to var (no target to infer from) or Object.
A lambda may only throw checked exceptions that its target functional
interface declares. The java.util.function interfaces declare none, so a
lambda calling a checked-exception method won't compile — you must handle it.
// won't compile: Files.readString throws IOException
// Function<Path,String> f = p -> Files.readString(p);
Function<Path,String> f = p -> {
try { return Files.readString(p); }
catch (IOException e) { throw new UncheckedIOException(e); }
};
Options: try/catch inside the lambda (often wrapping in an unchecked
exception), use an interface that does declare the exception (e.g. Callable),
or write a small "throwing function" wrapper utility. There's no
built-in checked-exception-friendly functional interface.
To avoid autoboxing. Using Function<Integer,Integer> boxes every int into
an Integer object on the heap — costly in hot loops and streams. The primitive
specializations work directly on int, long, and double, eliminating that
overhead.
| Specialization | Signature |
|---|---|
IntFunction<R> |
int -> R |
ToIntFunction<T> |
T -> int |
IntUnaryOperator |
int -> int |
IntBinaryOperator |
(int,int) -> int |
IntPredicate |
int -> boolean |
IntSupplier / IntConsumer |
() -> int / int -> void |
IntUnaryOperator square = x -> x * x; // no boxing
square.applyAsInt(5); // 25
IntStream, LongStream, and DoubleStream use these throughout, which is why
mapToInt(...).sum() is faster than mapping to boxed Integers.
Comparator is a functional interface, so you can write one as a lambda — but the
factory methods comparing, thenComparing, and reversed make it far
cleaner, and they take method references for the sort key.
// raw lambda
Comparator<Person> byAge = (a, b) -> Integer.compare(a.age(), b.age());
// idiomatic: key extractor + chaining
Comparator<Person> c = Comparator
.comparing(Person::lastName)
.thenComparing(Person::firstName)
.reversed();
people.sort(c);
comparing takes a Function (the key extractor) and builds the comparison for
you. For primitive keys use comparingInt/comparingDouble to avoid boxing.
This is one of the most common real-world uses of method references.
Inside a lambda, this refers to the enclosing instance — the object whose
method contains the lambda — not to the functional-interface object the lambda
becomes. This is because a lambda does not create a new scope; it's
lexically part of its surrounding method.
class Widget {
String name = "panel";
Runnable make() {
return () -> System.out.println(this.name); // "panel" — Widget's this
}
}
Contrast with an anonymous class, where this is the anonymous object itself, so
accessing the outer instance needs Widget.this. This lexical-this behavior
makes lambdas more intuitive for capturing enclosing state, and is a frequent
interview discriminator between the two.
More Streams & Functional interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.