Wednesday, May 10, 2023

Quiz yourself: Nested lambdas and Java thunks

Quiz Yourself, Oracle Java, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Learning, Oralce Java Preparation


Given the following three functional interfaces

interface Woo { public void wow(); }
interface Moo { public String mow(); }
interface Boo { public int bow(); }

and a class fragment

static void doIt(Woo w) {
  System.out.print("W");
  w.wow();
}
static String doIt(Moo m) {
  System.out.print("M");
  return m.mow();
}
static int doIt(Boo b) {
  System.out.print("B");
  return b.bow();
}
public static void main(String[] args) {
  doIt(() -> { doIt(() -> "done"); });
  doIt(() -> { doIt(() -> 1); });
}

What is the result? Choose one.

A. MMBB is printed.
B. WWWW is printed.
C. WMWB is printed.
D. MWBW is printed.
E. Compilation fails in the main method because the code is ambiguous.

Answer. This question investigates how lambdas take on a type that is compatible with their code and the context in which they are declared.

In this question, you are presented with several lambdas, including nested lambdas. That’s certainly a recipe for hard-to-read code, but the rules don’t change from the simple case. Look at these lambdas in turn and notice which interface each might be compatible with.

The two lambdas that are nested inside the others are () -> "done" and () -> 1.

It is clear that the first of these is compatible with the interface Moo because the method it defines takes no arguments and returns a String. Further, that first lambda is not compatible with either of the other interfaces because you can’t cast or promote a String to an int.

The second lambda is compatible with Boo because it declares a method that takes no arguments and returns an int. Again, casting or promotions can’t reconcile that lambda with either of the other two interface types.

Next, look at the outer lambdas: () -> { doIt(() -> "done"); } and () -> { doIt(() -> 1); }.

You know that the return type of the first enclosed lambda is String and that of the second is int. However, notice that these enclosing lambdas are block lambdas, that is, they include curly braces and must, therefore, define entire method bodies. However, the method bodies do not include return statements. So, in both cases, the enclosing lambda will invoke the enclosed lambda and ignore the returned value. The enclosing lambdas, therefore, have a void return type; as such, both are instances of the Woo interface.

At this point, you should have an instinct that since you have lambdas implementing all three interfaces, the output should contain all three letters: M, B, and W. If that instinct is correct, you can conclude that options A and B are looking unlikely. But that’s just a gut feeling, and while you might let that guide you if you’re running short of time in the exam, let’s trace the execution to determine what actually happens.

Think about the order in which the enclosing and enclosed lambdas bodies actually execute. In a Java method call, the arguments to a method invocation are always evaluated before the method is actually called. However, the value of a lambda is an object that contains the method the lambda describes. Java does not execute that method in constructing that object, and that means that when a lambda is passed as an argument, the method represented by the lambda has not been executed prior to the actual invocation of the method to which the lambda is being passed.

From this, you can tell that the very first lambda to be executed will be a Woo, which will print W. That one then delegates to the String-producing Moo and prints M. The process then repeats with a W from the second line’s enclosing lambda, followed by a B from the enclosed lambda that’s a Boo type. That results in the output WMWB.

Of course, the code compiles and runs, since all the lambdas validly and unambiguously satisfy one or other of the functional interfaces. Therefore, option E is incorrect. From the previous paragraph, you should conclude that the output is WMWB. Therefore, the correct answer is C and options A, B, and D are incorrect.

As a side note, you can use this technique to create the effect of lazy execution. When an exception is logged, for example, it is often quite computationally intensive to traverse all the frames in a stack, collect data, and concatenate the data into a log message—and very often, that log message is never used because the filtering level abandons it.

Java’s logging APIs allow passing a Supplier<String> to the log methods, so that if the message will not be used, it need never be evaluated. This idea is a design pattern that has a curious name; the pattern is commonly called a thunk. (It’s like the cartoonish past tense of to think, as in, “I had a thought, and the thought that I thunk was …” It’s very silly; don’t ask us why it exists, because we don’t know!)

This technique is sometimes implemented by a language (for example, Scala) such that the programmer simply writes a block of code that is wrapped in a thunk before being passed into an as-yet-unexecuted method. This is typically described as passing parameters by name.

It’s also interesting to consider what would have happened if the code had included explicit return statements.

public static void main(String[] args) {
  doIt(() -> { return doIt(() -> "done"); });
  doIt(() -> { return doIt(() -> 1); });
}

In this form, all four lambdas would return a value. The two in the first line return String and are, therefore, instances of Moo. The second two return int and are instances of Boo. The modified code would then print MMBB.

Conclusion. The correct answer is option C.

Source: oracle.com

Related Posts

0 comments:

Post a Comment