Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 Consumer/Function Lambda Ambiguity

I have an overloaded method that takes a Consumer and a Function object respectively and returns a generic type that matches the corresponding Consumer/Function. I thought this would be fine, but when I try to call either method with a lambda expression I get an error indicating the reference to the method is ambiguous.

Based on my reading of JLS §15.12.2.1. Identify Potentially Applicable Methods: it seems like the compiler should know that my lambda with a void block matches the Consumer method and my lambda with a return type matches the Function method.

I put together the following sample code that fails to compile:

import java.util.function.Consumer; import java.util.function.Function;  public class AmbiguityBug {   public static void main(String[] args) {     doStuff(getPattern(x -> System.out.println(x)));     doStuff(getPattern(x -> String.valueOf(x)));   }    static Pattern<String, String> getPattern(Function<String, String> function) {     return new Pattern<>(function);   }    static ConsumablePattern<String> getPattern(Consumer<String> consumer) {     return new ConsumablePattern<>(consumer);   }    static void doStuff(Pattern<String, String> pattern) {     String result = pattern.apply("Hello World");     System.out.println(result);   }    static void doStuff(ConsumablePattern<String> consumablePattern) {     consumablePattern.consume("Hello World");   }    public static class Pattern<T, R> {     private final Function<T, R> function;      public Pattern(Function<T, R> function) {       this.function = function;     }      public R apply(T value) {       return function.apply(value);     }   }    public static class ConsumablePattern<T> {     private final Consumer<T> consumer;      public ConsumablePattern(Consumer<T> consumer) {       this.consumer = consumer;     }      public void consume(T value) {       consumer.accept(value);     }   } } 

I also found a similar stackoverflow post that turned out to be a compiler bug. My case is very similar, though a bit more complicated. To me this still looks like a bug, but I wanted to make sure I am not misunderstanding the language spec for lambdas. I'm using Java 8u45 which should have all of the latest fixes.

If I change my method calls to be wrapped in a block everything seems to compile, but this adds additional verbosity and many auto-formatters will reformat it into multiple lines.

doStuff(getPattern(x -> { System.out.println(x); })); doStuff(getPattern(x -> { return String.valueOf(x); })); 
like image 558
johnlcox Avatar asked Jun 01 '15 23:06

johnlcox


People also ask

Which is correct about Java 8 lambda expression?

Q 6 - Which of the following is correct about Java 8 lambda expression? A - Using lambda expression, you can refer to final variable or effectively final variable (which is assigned only once).

What is consumer in lambda expression?

Interface Consumer<T> This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. @FunctionalInterface public interface Consumer<T> Represents an operation that accepts a single input argument and returns no result.

What is consumer and BiConsumer in Java 8?

Interface BiConsumer<T,U>Represents an operation that accepts two input arguments and returns no result. This is the two-arity specialization of Consumer . Unlike most other functional interfaces, BiConsumer is expected to operate via side-effects.

Can we write lambda without functional interface?

You do not have to create a functional interface in order to create lambda function.


1 Answers

This line is definitely ambiguous:

doStuff(getPattern(x -> String.valueOf(x))); 

Reread this from the linked JLS chapter:

A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:

  • The arity of the target type's function type is the same as the arity of the lambda expression.

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

  • If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).

In your case for Consumer you have a statement expression as any method invocation can be used as statement expression even if the method is non-void. For example, you can simply write this:

public void test(Object x) {     String.valueOf(x); } 

It makes no sense, but compiles perfectly. Your method may have a side-effect, compiler doesn't know about it. For example, were it List.add which always returns true and nobody cares about its return value.

Of course this lambda also qualifies for Function as it's an expression. Thus it's ambigue. If you have something which is an expression, but not a statement expression, then the call will be mapped to Function without any problem:

doStuff(getPattern(x -> x == null ? "" : String.valueOf(x))); 

When you change it to { return String.valueOf(x); }, you create a value-compatible block, so it matches the Function, but it does not qualify as a void-compatible block. However you may have problems with blocks as well:

doStuff(getPattern(x -> {throw new UnsupportedOperationException();})); 

This block qualifies both as a value-compatible and a void-compatible, thus you have an ambiguity again. Another ambigue block example is an endless loop:

doStuff(getPattern(x -> {while(true) System.out.println(x);})); 

As for System.out.println(x) case it's a little bit tricky. It surely qualifies as statement expression, so can be matched to Consumer, but seems that it matches to expression as well as spec says that method invocation is an expression. However it's an expression of limited use like 15.12.3 says:

If the compile-time declaration is void, then the method invocation must be a top level expression (that is, the Expression in an expression statement or in the ForInit or ForUpdate part of a for statement), or a compile-time error occurs. Such a method invocation produces no value and so must be used only in a situation where a value is not needed.

So compiler perfectly follows the specification. First it determines that your lambda body is qualified both as an expression (even though its return type is void: 15.12.2.1 makes no exception for this case) and a statement expression, so it's considered an ambiguity as well.

Thus for me both statements compile according to the specification. ECJ compiler produces the same error messages on this code.

In general I'd suggest you to avoid overloading your methods when your overloads has the same number of parameters and has the difference only in accepted functional interface. Even if these functional interfaces have different arity (for example, Consumer and BiConsumer): you will have no problems with lambda, but may have problems with method references. Just select different names for your methods in this case (for example, processStuff and consumeStuff).

like image 154
Tagir Valeev Avatar answered Sep 30 '22 14:09

Tagir Valeev