Java 8 features at depth, how lambdas work (invokedynamic) and differ from anonymous classes, functional interfaces with default methods, the built-in java.util.function types, method references, this/super in lambdas, variable capture, checked exceptions in lambdas, and synchronization inside lambdas.
Published September 25, 2026
At 2–5 years, "what is a lambda?" isn't enough. Explain how lambdas are compiled, what they capture, and how they behave differently from anonymous classes (this, scope, serialization). A couple of those details will set you apart.
Short answer: Java 8 introduced:
java.util.function);Optional;java.time API;CompletableFuture;Together they brought functional-style, declarative data processing to Java, and made asynchronous composition practical. They also let the JDK evolve existing interfaces (such as Collection.stream()) without breaking every implementation.
Learn it in depth → Lambda Expressions
Short answer: A lambda is an anonymous function, (params) -> body, that implements the single abstract method of a functional interface. Its benefits: much less boilerplate, behaviour passed as data (strategies, callbacks, comparators), and it's the foundation of streams and CompletableFuture pipelines.
Comparator<Order> byTotal = (a, b) -> a.total().compareTo(b.total());
Predicate<Order> isLarge = o -> o.total().compareTo(new BigDecimal("10000")) > 0;
Runnable task = () -> log.info("tick");
Key points to cover:
invokedynamic with LambdaMetafactory to create the functional-interface instance at runtime. No .class file is generated per lambda, and non-capturing lambdas are cached as singletons.Short answer:
| Lambda | Anonymous class | |
|---|---|---|
| Can implement | Only a functional interface | Any interface (several methods) or abstract class |
this means | The enclosing instance | The anonymous object itself |
| New scope? | No: can't redeclare the enclosing method's local names | Yes: can shadow variables |
| State (fields) | None | Can have fields |
| Compilation | invokedynamic + a synthetic method | A separate Outer$1.class |
| Instance per evaluation | Non-capturing lambdas are reused | Always a new object |
class Button {
String name = "save";
void wire() {
Runnable l = () -> System.out.println(this.name); // "save": the enclosing Button
Runnable a = new Runnable() {
String name = "anon";
public void run() { System.out.println(this.name); } // "anon": the anonymous object
};
}
}
Short answer: Choose an anonymous class when you must:
this) inside its body, for example to unregister itself as a listener.Otherwise, use a lambda (or a method reference).
Short answer: Yes. A functional interface needs exactly one abstract method. Any number of default and static methods is fine, and so are abstract methods that just redeclare public Object methods (like equals). Comparator is the best example: one abstract method (compare), plus many default methods (reversed, thenComparing, …).
@FunctionalInterface
interface Rule {
boolean test(Order o); // the single abstract method
default Rule and(Rule other) { return o -> test(o) && other.test(o); }
default Rule negate() { return o -> !test(o); }
boolean equals(Object obj); // an Object method: doesn't count
}
Common trap: answering "no". The source of this question in many prep sheets even contradicts itself. The rule counts abstract methods only.
Short answer: An interface with exactly one abstract method (a SAM type), which lambdas and method references can implement. @FunctionalInterface is optional, but it makes the compiler enforce the rule, so nobody accidentally adds a second abstract method later.
Key points to cover:
Runnable, Callable, Comparator, and Spring's RowMapper and TransactionCallback.Short answer: They live in java.util.function. The core four and their families:
| Interface | Signature | Typical use |
|---|---|---|
Supplier<T> | () → T | Lazy values, factories (orElseGet) |
Consumer<T> / BiConsumer<T,U> | T → void | Side effects (forEach) |
Function<T,R> / BiFunction<T,U,R> | T → R | Transformations (map) |
Predicate<T> / BiPredicate<T,U> | T → boolean | Filters (filter, removeIf) |
UnaryOperator<T> / BinaryOperator<T> | T → T, (T,T) → T | replaceAll, reduce |
| Primitive variants | IntPredicate, ToLongFunction, IntBinaryOperator, … | Avoid boxing |
Key points to cover:
Function.andThen/compose, Predicate.and/or/negate/not, Comparator.thenComparing.Short answer: A method reference is a shorter form of a lambda whose body only calls an existing method. There are four kinds:
| Kind | Example | Equivalent lambda |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Instance method of a particular object | System.out::println | x -> System.out.println(x) |
| Instance method of an arbitrary object of a type | String::toUpperCase | s -> s.toUpperCase() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
Key points to cover:
System.out::println, this::handle) evaluates its receiver once, when the reference is created. A lambda re-evaluates it on each call.Learn it in depth → Method References
this and super inside a lambda?Short answer: Yes. Because a lambda doesn't introduce a new scope or object identity, this and super refer to the enclosing class instance and its superclass, exactly as they would in the surrounding method. (Inside an anonymous class, this would be the anonymous object instead.)
class ReportService extends BaseService {
void schedule(ScheduledExecutorService ses) {
ses.schedule(() -> super.logStart(this.name()), 5, TimeUnit.SECONDS); // enclosing object and its parent
}
}
Key points to cover:
this keeps the enclosing object reachable for as long as the lambda is. Be careful with long-lived callbacks.Short answer: A lambda can capture local variables and parameters from the enclosing scope, but only if they're final or effectively final. The lambda receives a copy of the value. It can also read and modify fields (instance or static), because those are accessed through the object, not copied.
Key points to cover:
Short answer: It can throw any unchecked exception. It can throw a checked exception only if the functional interface's method declares it, and the standard Function and Consumer don't. The options:
UncheckedIOException.Result/Either type) instead of throwing.@FunctionalInterface
interface ThrowingFunction<T, R> { R apply(T t) throws Exception; }
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
return t -> {
try { return f.apply(t); }
catch (RuntimeException e) { throw e; }
catch (Exception e) { throw new CompletionException(e); }
};
}
List<String> contents = paths.stream().map(unchecked(Files::readString)).toList();
Short answer: It's a compile error: "local variables referenced from a lambda expression must be final or effectively final". The same applies to modifying the variable after the lambda captures it anywhere in the method.
int count = 0;
list.forEach(x -> count++); // ❌ compile error
AtomicInteger counter = new AtomicInteger(); // workaround, but prefer a stream reduction:
list.forEach(x -> counter.incrementAndGet());
long count2 = list.stream().filter(this::isValid).count(); // ✅ idiomatic
Common trap: using a one-element array (int[] c = {0}) to sneak around the rule. It compiles, but it isn't thread-safe in parallel streams, and it hides intent.
synchronized inside a lambda?Short answer: Yes, a synchronized block inside the lambda body is perfectly legal: () -> { synchronized (lock) { … } }. What you can't do is mark the lambda itself synchronized, because lambdas have no method modifiers. And synchronized (this) inside a lambda locks the enclosing instance, not the lambda.
Object lock = new Object();
Runnable increment = () -> {
synchronized (lock) { // ✅ compiles and works
sharedCounter++;
}
};
Common trap: claiming "synchronized can't be used in lambdas". It's a widespread wrong answer. Better design still avoids shared mutable state: use AtomicLong/LongAdder, or a reduction.
Q: Are lambdas objects? Do they have a class?
A: Yes. At runtime, a lambda is an instance of a hidden class that implements the functional interface. Don't rely on its identity, toString() or class name.
Q: Can a lambda be serialized?
A: Only if its target type is Serializable (for example, a cast to (Runnable & Serializable)). That's fragile, and generally discouraged.
Q: What's the performance cost of lambdas? A: Tiny. Non-capturing lambdas are cached singletons. Capturing lambdas allocate a small object, which escape analysis often eliminates. The first call to each lambda has a one-off bootstrap cost.
Q: What does Predicate.not add?
A: Since Java 11, Predicate.not(String::isBlank) negates a method reference, so filter(not(String::isBlank)) reads cleanly.