Skip to content

Lambdas & Functional Interfaces Interview Questions & Answers

21 questions Updated 2026-06-20 Share:

Java lambdas interview questions — lambda syntax, functional interfaces, the java.util.function package, method references, effectively final capture, and @FunctionalInterface.

Read the in-depth guideJava Lambdas & Functional Interfaces — A Practical Guide(opens in new tab)
21 of 21

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 ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel