0

Given this method:

private static Integer return0() {
    return 0;
}

I discovered a weird property of the following lambda expression:

() -> return0();

Does it actually return the value from the function it calls (which would make it a Supplier-Interface) or does it not return the value but only calls the function and returns void (which would make it a Runnable-Interface). Intuitively, I would expect the first case to be correct but could live with the second.

When trying to assign the statement:

    Supplier<Integer> supplier2 = () -> return0();
    Runnable runnable2 = () -> return0();

It turns out both lines do compile! Why would they allow that? It is completely ambiguous and really confusing!

EDIT: Here is more code to demonstrate what I mean by confusing/ambigous:

public static void main(String[] args) {
    callMe(() -> return0());
}

private static Integer return0() {
    return 0;
}

private static void callMe(Supplier<Integer> supplier) {
    System.out.println("supplier!");
}

private static void callMe(Runnable runnable) {
    System.out.println("runnable!");
}

This all compiles well and upon execution prints "supplier!". I do not find it particularly intuitive that the first method is chosen but rather arbitrary.

schrobe
  • 767
  • 2
  • 8
  • 29
  • 4
    A return value can be igored, making it `void`. This is compatible. – Boris the Spider Jun 09 '18 at 20:27
  • 4
    "It is completely ambiguous" Why is it ambiguous? You are providing extra context in the variable type. – Andy Turner Jun 09 '18 at 20:31
  • @AndyTurner because if I e.g. do not assign it to a variable but instead pass it to an overloaded function that takes both types as parameters it is undecided which function to call. – schrobe Jun 09 '18 at 20:34
  • What do you mean with "can be ignored"? In this case for example it cannot be ignored even though I also give the contextual information in the variable type:`Supplier supplier2 = () -> {return 0;}; Runnable runnable2 = () -> {return 0};` – schrobe Jun 09 '18 at 20:36
  • You sure? Have you tried it with overloaded methods? – Boris the Spider Jun 09 '18 at 20:36
  • 1
    I think that when you choose to overload a method with Runnable and Supplier parameters, especially with the intent of providing them with lambdas, you're deliberately taking actions that you know will remove all chances of being intuitive. Overloading should always be accompanied with wondering whether the use of the methods will work as intended. When overloading with different numbers of parameters you can feel okay... Otherwise you really need to question yourself rather than the language nor anything else. – kumesana Jun 09 '18 at 21:28
  • @kumesana The `java.util.concurrent.ExecutorService#submit()` does exactly that: it is overloaded with Runnable and Callable (which is the same as Supplier). https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html – schrobe Jun 09 '18 at 21:39
  • 1
    Yes, and unless you're trying to, it produces nothing counterintuitive. But I said you need to ask yourself questions. Especially if you know lambdas exist and you intend to use them. They didn't when they made ExecutorService, and now it's possible to fall in traps... When you make not-too-smart decisions regarding the lambdas you use and your expectations. – kumesana Jun 09 '18 at 22:51

3 Answers3

3

The relevant part of the spec is Sec 15.27.3 (emphasis mine):

A lambda expression is congruent with a function type if all of the following are true:

  • The function type has no type parameters.

  • The number of lambda parameters is the same as the number of parameter types of the function type.

  • If the lambda expression is explicitly typed, its formal parameter types are the same as the parameter types of the function type.

  • If the lambda parameters are assumed to have the same types as the function type's parameter types, then:

    • If the function type's result is void, the lambda body is either a statement expression (§14.8) or a void-compatible block.

    • If the function type's result is a (non-void) type R, then either i) the lambda body is an expression that is compatible with R in an assignment context, or ii) the lambda body is a value-compatible block, and each result expression (§15.27.2) is compatible with R in an assignment context.

Your lambda body is a statement expression, and the function type's result is void.

In other words, it would be fine for you to write:

return0();

and ignore the return value in "regular" code, so it's fine to ignore the result value in a lambda too.


In terms of the question over ambiguity of overloads, there is no ambiguity in this case (it's easy to construct a case where there is ambiguity, e.g. another overload with a parameter that looks like Supplier but is a different interface, i.e. takes no parameters, returns a value).

You would have to read the spec in detail for the precise reasoning, but I think the most relevant section is Sec 15.12, which describes method invocation expressions, and the most useful quote from that is in Sec 15.12.2.5, which deals with selecting the most-specific overload:

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error.

You can use a Supplier<Integer> in place of a Runnable (with a bit of a hand-wavy fudge) because you can simply ignore the return value; you can't use a Runnable in place of a Supplier<Integer> because it doesn't have a return value.

So a method taking the Supplier<Integer> is more specific than the method taking the Runnable, hence that is the one which is invoked.

Andy Turner
  • 137,514
  • 11
  • 162
  • 243
  • Ah thanks, that's what I was looking for! But I still think it's an odd choice to allow a statement expression in that case. Because in the example described above where you have overloaded method to my understanding both could be called. It's a matter of "which fits better" and this section does not say anything about that, does it? – schrobe Jun 09 '18 at 20:49
  • Thanks you for the thorough explanation! I guess I can accept that and at the same time it does comforts me that I was not completely wrong with the feeling that something there is "kind of" arbitrary. – schrobe Jun 09 '18 at 21:47
2

If you get confused with lambda expressions, replace them with anonymous classes for a better understanding (IntelliJ IDEA can easily help you with that). The following code snippets are completely valid:

Supplier<Integer> supplier2 = () -> return0() is equivalent to:

Supplier<Integer> supplier2 = new Supplier<Integer>() {
    @Override
    public Integer get() {
        return return0();
    }
};

Runnable runnable2 = () -> return0() is equivalent to:

Runnable runnable2 = new Runnable() {
    @Override
    public void run() {
        return0();
    }
};
Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90
  • Fair enough, this does makes sense now. Then maybe it is due to the compact form that makes it confusing. – schrobe Jun 09 '18 at 20:52
0
public static void main(String[] args) throws Exception
{
    Supplier<Integer> consumer2 = Trial::return0;
    Runnable runnable2 = Trial::return0;
    run(Trial::return0);
}

private static Integer return0() {
    return 0;
}

private static int run(Supplier<Integer> a)
{
    System.out.println("supplier");
    return a.get();
}

private static void run(Runnable r)
{
    System.out.println("runnable");
    r.run();
}

As far as method overloading is concerned, this code in class Trial prints "supplier".

gagan singh
  • 1,591
  • 1
  • 7
  • 12