Lambda Expressions and Functional Interfaces practice questions

From OCP Java SE 21 (1Z0-830) · 18 questions on this topic

Lambda Expressions and Functional Interfaces practice questions from OCP Java SE 21 (1Z0-830). This pack has 18 questions tagged Lambda Expressions and Functional Interfaces, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Lambda Expressions and Functional Interfaces

  1. Question 1

    The program below chains two `Consumer<String>` instances using **`andThen`**. What does it print to standard output? ```java import java.util.function.Consumer; public class Main { public static void main(String[] args) { Consumer<String> upper = s -> System.out.print(s.toUpperCase()); Consumer<String> lower = s -> System.out.print(s.toLowerCase()); Consumer<String> both = upper.andThen(lower); both.accept("Hello"); } } ```

    1. A. HELLOhelloCorrect answer

      `Consumer.andThen(after)` returns a composed consumer that performs the receiver's action first, then the `after` action on the same argument (Consumer Javadoc). `upper` runs first printing `HELLO`, then `lower` prints `hello`; both use `System.out.print`, so no newline separates them.

    2. B. helloHELLO

      Assumes the argument to `andThen` executes before the receiver — the opposite of how `andThen` works. Producing this output would require `lower.andThen(upper)` instead.

    3. C. HELLO

      Assumes `andThen` applies only the receiver and silently discards the argument consumer. The composed consumer returned by `andThen` runs both actions in order.

    4. D. Compilation fails

      `Consumer<T>` declares `andThen(Consumer<? super T> after)` as a default method and `accept(T)` as its single abstract method. The code is type-correct and compiles cleanly.

    Explanation

    `Consumer.andThen(after)` produces a composed `Consumer` that applies the receiver *before* `after`, both receiving the same input. Because neither lambda appends a newline (both call `System.out.print`), the two partial outputs are concatenated on a single line with the uppercase portion preceding the lowercase. Swapping which consumer is the receiver and which is the argument reverses the two halves. Treating `andThen` as though it ignores its argument would leave only the first half. `Consumer<T>` provides `andThen` as a default method, so no compilation error arises.

  2. Question 2

    Which two statements about the built-in java.util.function interfaces are correct? (Choose two.)

    1. A. Supplier<T> takes no arguments and returns a valueCorrect answer

      Supplier<T> declares T get(): no input, one output.

    2. B. Consumer<T> accepts a single argument and returns no resultCorrect answer

      Consumer<T> declares void accept(T t): one input, no output, the mirror image of Supplier.

    3. C. Predicate<T> returns an int so results can be compared

      Predicate<T> returns a primitive boolean from test(T); the int-returning comparison contract belongs to Comparator, not Predicate.

    4. D. UnaryOperator<T> extends Consumer<T>

      UnaryOperator<T> extends Function<T,T>, a function whose input and output types match, not Consumer.

    Explanation

    Anchor the core interfaces by shape: a supplier takes nothing and returns a value, a consumer takes a value and returns nothing, a function maps an input to an output, and a predicate maps an input to a boolean. The two accurate statements describe the no-input/one-output and one-input/no-output mirror pair. Every other interface in java.util.function is a specialization of one of these, so derive it from the name rather than memorizing.

  3. Question 3

    A lambda declared inside an enhanced for loop captures the loop variable. Each lambda is stored and invoked after the loop has finished. What does this print? ```java import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; public class Main { public static void main(String[] args) { List<Supplier<String>> tasks = new ArrayList<>(); for (String s : List.of("a", "b", "c")) { tasks.add(() -> s + "!"); } StringBuilder out = new StringBuilder(); for (Supplier<String> t : tasks) { out.append(t.get()); } System.out.println(out); } } ```

    1. A. Compilation fails: the loop variable s is not effectively final

      Generalises the capture rule too far. The enhanced-for variable is declared fresh each iteration and assigned exactly once, so it IS effectively final and captures legally; it is the classic `for` index (reassigned by i++) that fails this test.

    2. B. Compilation fails: s must be explicitly declared final to be captured

      The pre-Java-8 rule. Since Java 8, EFFECTIVELY final is enough for capture and the `final` keyword is optional.

    3. C. c!c!c!

      Assumes all three lambdas share one mutable loop variable and read its final value — the closure-over-a-var behaviour of languages like JavaScript. Java captures by value, and the enhanced-for variable is a fresh, distinct variable each iteration.

    4. D. a!b!c!Correct answer

      The enhanced-for statement declares `s` fresh each iteration, assigned once and never reassigned, so it is effectively final and each lambda captures its own `s`; invoking them yields a!, b!, c! → a!b!c!.

    Explanation

    Trace: the enhanced for statement declares `s` *inside* the loop — each iteration creates a brand-new local variable that is assigned exactly once and never reassigned. That makes it effectively final, so the lambda may capture it, and each of the three lambdas captures its own separate `s`. Invoking them later yields `a!`, `b!`, `c!`, which append to `a!b!c!`. Why the others are wrong: `c!c!c!` encodes the belief that all three lambdas share one loop variable and read its final value — the closure-over-a-mutable-loop-variable behaviour of languages like JavaScript's `var`. Java captures by value, and here there are three distinct variables anyway. `Compilation fails: the loop variable s is not effectively final` generalises the real restriction too far. The variable that is *not* effectively final is the index of a basic `for (int i = 0; i < 3; i++)`, because `i++` reassigns it; capturing that one really does fail with "local variables referenced from a lambda expression must be final or effectively final". The enhanced-for variable is never reassigned. `Compilation fails: s must be explicitly declared final to be captured` is the pre-Java-8 rule. Since Java 8, *effectively* final is enough — the `final` keyword is optional. Exam tip: for capture, ask one question only — is this variable ever assigned more than once? An enhanced-for variable is fresh per iteration, so it passes; a classic `for` index is bumped each pass, so it fails. The standard workaround for the indexed loop is to copy the index into a new local inside the body.

  4. Question 4

    Given `String greeting = "hello"; Supplier<String> s = greeting::toUpperCase;` — which kind of method reference is `greeting::toUpperCase`?

    1. A. An unbound instance-method reference — the receiver is supplied later as an argument

      An unbound reference is written with the type (String::toUpperCase) and needs the receiver as its first argument, so it matches Function<String,String>, not Supplier.

    2. B. A static-method reference to String.toUpperCase

      toUpperCase is an instance method; a static reference like Integer::parseInt requires a static method.

    3. C. A constructor reference

      Constructor references use new, such as String::new, which this expression is not.

    4. D. A bound instance-method reference — the receiver greeting is captured when the reference is createdCorrect answer

      The expression before :: is an existing object (the variable greeting), so the receiver is fixed (bound) when the reference is created; needing no receiver parameter is why it fits Supplier<String>, which has zero inputs and one output.

    Explanation

    When the expression before :: is an existing object rather than a type, the receiver is captured at reference-creation time, making it a bound instance-method reference. Because the receiver is already fixed, no parameter is needed to supply one, which is exactly why it fits a zero-argument, one-result Supplier (JLS 21 15.13). Match the shape of the functional interface: a bound reference consumes no receiver parameter while an unbound one consumes an extra leading parameter.

  5. Question 5

    The following program attempts to capture a local variable inside a lambda expression. What is the result of compiling and running this program? ```java import java.util.function.Supplier; public class Main { public static void main(String[] args) { String greeting = "Hello"; Supplier<String> s = () -> greeting + " World"; greeting = "Hi"; System.out.println(s.get()); } } ```

    1. A. `Hello World`

      Assumes the lambda freezes greeting's value at definition time ('Hello') and that the later reassignment does not affect compilation. In reality, any reassignment of a captured local variable anywhere in the enclosing scope makes it non-effectively-final; the compiler rejects the program before a value is ever read or printed.

    2. B. Compilation failsCorrect answer

      greeting is reassigned after the lambda is defined, so it is not effectively final (JLS §4.12.4). JLS §15.27.2 requires every local variable used but not declared in a lambda body to be final or effectively final; violating this rule is a compile-time error. The compiler's effectively-final analysis covers the entire enclosing scope, so a reassignment that appears after the lambda definition in source order is equally disqualifying.

    3. C. `Hi World`

      Assumes Java lambdas capture by reference and always see the most recent value of the variable. Java lambdas capture by the value of an effectively-final variable; moreover, the reassignment to 'Hi' disqualifies greeting from capture entirely, so the program never reaches runtime.

    4. D. Compiles successfully and throws `NullPointerException` at runtime

      greeting is initialized to a non-null String literal and the lambda performs string concatenation, so no NullPointerException is possible. More fundamentally, the effectively-final rule is enforced at compile time, so the program never reaches runtime.

    Explanation

    A local variable accessed in a lambda body must be **effectively final** — never modified after its initial assignment (JLS §4.12.4). Because `greeting` is reassigned anywhere in the enclosing method after initialization, the compiler considers it non-effectively-final and rejects the program at compile time (JLS §15.27.2); the analysis covers the entire method scope, so a reassignment that appears after the lambda definition in source order is just as disqualifying as one that appears before it. Thinking the lambda freezes the captured value at definition time misreads what the effectively-final rule enforces; thinking it reads the latest value at invocation time confuses Java's strictly value-based capture with by-reference capture found in languages such as C++ or Python.

  6. Question 6

    A lambda writes into an array element and into a static field. What does this print? ```java import java.util.List; import java.util.function.Consumer; public class Main { static int total = 0; public static void main(String[] args) { int[] box = {0}; List<Integer> nums = List.of(1, 2, 3); Consumer<Integer> tally = n -> { box[0] += n; total += n * 2; }; nums.forEach(tally); System.out.println(box[0] + ":" + total); } } ```

    1. A. Compilation fails: a lambda cannot assign to the static field total

      Over-extends the effectively-final restriction to fields. Only local variables and parameters must be effectively final; a static field like total may be freely assigned from a lambda.

    2. B. 6:12Correct answer

      box is assigned once, so it is effectively final and capturable; box[0] += n mutates the array (summing to 6), and total is a static field freely writable from the lambda (2 + 4 + 6 = 12).

    3. C. Compilation fails: box is not effectively final because the lambda writes to it

      Confuses mutating an object's contents with reassigning the variable. box = new int[1] would break the rule; box[0] += n does not, and this one-element-array trick is the standard accumulation idiom.

    4. D. 0:12

      Assumes the captured array is copied so writes are lost to the caller. Capture copies the reference, so the lambda and main see the one array, and box[0] ends at 6.

    Explanation

    Trace: the effectively-final rule constrains the *variable*, not the object it points at. `box` is assigned once and never reassigned, so it is effectively final and capturable; `box[0] += n` mutates the array the reference points to, which the rule says nothing about. And the rule applies only to local variables — `total` is a static field, reachable through the class, so a lambda may freely assign to it. Over 1, 2, 3 the array accumulates 6 and the field accumulates 2 + 4 + 6 = 12, printing `6:12`. Why the others are wrong: `Compilation fails: box is not effectively final because the lambda writes to it` confuses mutating an object's contents with reassigning the variable. `box = new int[1];` inside the lambda would break the rule; `box[0] += n` does not. (This one-element-array trick is the standard idiom for accumulating from a lambda precisely because it is legal.) `Compilation fails: a lambda cannot assign to the static field total` over-extends the restriction to fields. Only local variables and parameters must be effectively final; instance and static fields are fair game. `0:12` assumes the captured array is copied, so writes inside the lambda are lost to the caller. Capture copies the *reference*, and both the lambda and `main` see the one array. Exam tip: when you see a write inside a lambda, ask what is on the left of the `=`. A bare local name — illegal. An array slot, a field, or a setter call on a captured object — perfectly legal, and a favourite way of smuggling mutable state past the rule.

  7. Question 7

    What is the output of the following program? ```java import java.util.function.IntUnaryOperator; public class Main { public static void main(String[] args) { IntUnaryOperator triple = n -> n * 3; IntUnaryOperator addTen = n -> n + 10; IntUnaryOperator composed = triple.andThen(addTen); System.out.println(composed.applyAsInt(5)); } } ```

    1. A. 25Correct answer

      IntUnaryOperator.andThen(after) applies the receiver first, then after: addTen.applyAsInt(triple.applyAsInt(5)) = addTen.applyAsInt(15) = 25. This mirrors the same left-to-right contract as Function.andThen() but operates on unboxed int values via applyAsInt(). (IntUnaryOperator.andThen javadoc)

    2. B. 15

      The result of applying only triple (5 * 3 = 15) and ignoring the andThen step — the misconception that andThen is decorative or applies only when a condition is met. andThen always chains a second operator unconditionally.

    3. C. 45

      The result of applying compose order rather than andThen order: triple.applyAsInt(addTen.applyAsInt(5)) = triple.applyAsInt(15) = 45. IntUnaryOperator.compose(before) applies its argument first and then the receiver; andThen(after) does the opposite.

    4. D. Compilation fails

      IntUnaryOperator is a standard functional interface in java.util.function with default methods andThen() and compose(); lambda literals assigning to it are valid; and applyAsInt(int) is its correct abstract-method signature. The code compiles and runs without error.

    Explanation

    IntUnaryOperator is the int-primitive specialisation of UnaryOperator<Integer>; it uses applyAsInt(int) to avoid autoboxing overhead. Like Function.andThen(), IntUnaryOperator.andThen(after) applies the receiver operator first and feeds its result to after — triple executes before addTen, not in reverse. The compose(before) default method reverses this application order, and stopping after the first operator entirely omits the chained step.

  8. Question 8

    What does this print? ```java import java.util.function.*; public class Main { public static void main(String[] args) { Predicate<String> longer = s -> s.length() > 3; Predicate<String> startsJ = s -> s.startsWith("j"); System.out.println(longer.and(startsJ).test("java") + " " + longer.negate().test("jab")); } } ```

    1. A. true false

      The second value false would require "jab" to satisfy longer; it does not, and negate() inverts that false to true.

    2. B. false true

      The first value false would require "java" to fail one of the two tests, but it passes both the length and prefix checks.

    3. C. true trueCorrect answer

      For "java" the length (4 > 3) and prefix (startsWith "j") tests both hold, so the and-combined predicate is true; for "jab" length 3 > 3 is false and negate() flips it to true, giving true true.

    4. D. false false

      Both printed values are actually true, as traced, so neither is false.

    Explanation

    Combining predicates with and requires both to hold, and negate() returns a new predicate that inverts the original's result. "java" satisfies both the length and prefix tests, and "jab" fails the length test so its negation is true. Note that and/or short-circuit like && and ||, and negate() leaves the original predicate unchanged and reusable.

Practise all 18 Lambda Expressions and Functional Interfaces questions

OCP Java SE 21 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 21

Other topics in this pack