7

The code snippet shown below works. However, I'm not sure why it works. I'm not quite following the logic of how the lambda function is passing information to the interface.

Where is control being passed? How is the compiler making sense of each n in the loop and each message created?

This code compiles and gives the expected results. I'm just not sure how.

import java.util.ArrayList;
import java.util.List;

public class TesterClass {

    public static void main(String[] args) {

        List<String> names = new ArrayList<>();

        names.add("Akira");
        names.add("Jacky");
        names.add("Sarah");
        names.add("Wolf");

        names.forEach((n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        });
    }

    interface SayHello {
        void speak(String message);
    }
}
TeWu
  • 5,928
  • 2
  • 22
  • 36
Brodiman
  • 149
  • 2
  • 9

2 Answers2

7

The SayHello is a Single Abstract Method interface which has a method that takes a string and returns void. This is analogous to a consumer. You are just providing an implementation of that method in form of a consumer which is similar to the following anonymous inner class implementation.

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

names.forEach(n -> sh.speak(n));

Fortunately, your interface has a single method such that the type system (more formally, the type resolution algorithm) can infer its type as SayHello. But if it were to have 2 or more methods, this would not be possible.

However, a much better approach is to declare the consumer before the for-loop and use it as shown below. Declaring the implementation for each iteration creates more objects than necessary and seems counter-intuitive to me. Here's the enhanced version using method references instead of lambda. The bounded method reference used here calls the relevant method on the hello instance declared above.

SayHello hello = message -> System.out.println("Hello " + message);
names.forEach(hello::speak);

Update

Given that for stateless lambdas that does not capture anything from their lexical scope only once instance will ever be created, both of the approaches merely create one instance of the SayHello, and there's no any gain following the suggested approach. However this seems to be an implementation detail and I didn't know it until now. So a much better approach is just to pass in a consumer to your forEach as suggested in the comment below. Also note that all these approaches creates just one instance of your SayHello interface while the last one is more succinct. Here's how it looks.

names.forEach(message -> System.out.println("Hello " + message));

This answer will give you more insight on that. Here's the relevant section from the JLS §15.27.4: Run-Time Evaluation of Lambda Expressions

These rules are meant to offer flexibility to implementations of the Java programming language, in that:

  • A new object need not be allocated on every evaluation.

In fact, I initially thought every evaluation creates a new instance, which is wrong. @Holger thanks for pointing that out, good catch.

Ravindra Ranwala
  • 20,744
  • 6
  • 45
  • 63
  • Ok, I think I understand. I simply created a version of the Consumer interface. But, since I put it in the loop, I created a separate object on each loop, eating up memory. Whereas, your implementation creates that one object, and swaps out "message" on each loop. Thank you for the explanation! – Brodiman Nov 03 '19 at 02:41
  • 1
    Yes, it is always good if you can use an existing functional interface rather than rolling out your own. In this case you can use `Consumer` instead of having `sayHello` interface. – Ravindra Ranwala Nov 04 '19 at 06:43
  • 1
    It doesn’t matter where you place the line `SayHello hello = message -> System.out.println("Hello " + message);`, there will be only one `SayHello` instance. Given that even that one object is obsolete here anyway, as the same is achievable via `names.forEach(n -> System.out.println("Hello " + n))`, there is no improvement in your “enhanced version”. – Holger Nov 04 '19 at 15:20
  • @Holger I have updated the answer with your suggestion. Really appreciate it. So all the implementations create one single instance of SayHello while the one suggested by you is more compact? Isn't that so? – Ravindra Ranwala Nov 05 '19 at 02:49
0

The signature of foreach is soemthing like below:

void forEach(Consumer<? super T> action)

It takes in an object of Consumer. The Consumer interface is a functional interface (an interface with a single abstract method). It accepts an input and returns no result.

Here's the definition:

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

Therefore, any implementation, the one in your case can be written in two ways:

Consumer<String> printConsumer = new Consumer<String>() {
    public void accept(String name) {
        SayHello hello = (message) -> System.out.println("Hello " + message);
        hello.speak(n);
    };
};

OR

(n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        }

Similarly the code for the SayHello class can be written as

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

OR

SayHello hello = message -> System.out.println("Hello " + message);

So names.foreach internally first calls consumer.accept method and runs its lambda/anonymous implementation which creates and calls the SayHello lambda implementation and so on. Using lambda expression the code is very compact and clear. Only thing you need to understand how its working i.e in your case it was using Consumer interface.

So final flow : foreach -> consumer.accept -> sayhello.speak

Nishant Lakhara
  • 2,295
  • 4
  • 23
  • 46