Java8:lambda表达式和重载方法的歧义 [英] Java8: ambiguity with lambdas and overloaded methods

查看:98
本文介绍了Java8:lambda表达式和重载方法的歧义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



假设我有一个函数接口A ,一个抽象类B 和一个类C A B 作为参数:

  public interface A {
void invoke(String arg);


public abstract class B {
public abstract void invoke(String arg);


public class C {
public void apply(A x){}
public B apply(B x){return x; }
}

然后我可以将lambda传递给 c。应用,并正确解析为 c.apply(A)

  C c = new C(); 
c.apply(x - > System.out.println(x));

但是当我更改重载 B 作为泛型版本的参数,编译器报告这两个重载是不明确的。

  public class C {
public void apply(A x){}
public< T extends B> T apply(T x){return x; }我认为编译器会看到 T



必须是 B 的子类,它不是一个功能接口。为什么它不能解决正确的方法?

解决方案

重载解析和类型推断的交集有很多复杂性。 lambda规范的当前草案具有所有的细节。 F节和G节分别介绍重载分辨率和类型推断。我不会假装理解这一切。然而,引言中的总结部分是可以理解的,我建议人们阅读它们,尤其是F和G部分的摘要,以便了解这方面的情况。

为简要概括这些问题,请考虑在存在重载方法的情况下调用某些参数的方法。重载解析必须选择正确的方法来调用。该方法的形状(参数或参数数量)是最重要的;很显然,带有一个参数的方法调用无法解析为带有两个参数的方法。但重载的方法通常具有相同数量的不同类型的参数。在这种情况下,类型开始变得重要。



假设有两种重载方法:

  void foo(int i); 
void foo(String s);

和一些代码有以下方法调用:

  foo(hello); 

显然,根据传递参数的类型,这解决了第二种方法。但是如果我们正在做重载解析,而且参数是lambda? (尤其是那些类型是隐式的,依赖于类型推断来建立类型的类型。)回想一下,lambda表达式的类型是从目标类型中推断出来的,也就是这个上下文中预期的类型。不幸的是,如果我们有重载的方法,我们没有一个目标类型,直到我们解决了我们要调用的重载方法。但是由于我们还没有lambda表达式的类型,我们不能在重载解析时使用它的类型来帮助我们。



让我们看看这里的例子。考虑示例中定义的接口 A 和抽象类 B 。我们有包含两个重载的类 C ,然后一些代码调用 apply 方法并将它传递给lambda:< (b)


$ b

 .apply(x  - > System.out.println(x)); 

apply 过载的数量相同参数。参数是一个lambda,它必须匹配一个功能接口。 A B 是实际的类型,所以显示 A 是一个函数接口,而 B 不是,因此重载解析的结果是 apply(A)。在这一点上,我们现在有一个针对lambda的目标类型 A ,并为 x 类型推断出发。



现在变体:

  public void apply(A a)
public< T extends B> T应用(T t)

c.apply(x - > System.out.println(x));

代替实际的类型,的第二个重载应用是一个泛型类型变量 T 。我们还没有进行类型推断,因此我们不考虑 T ,至少在重载解析完成之后才会这样做。因此,这两种重载仍然适用,也不是最具体的,编译器会发出调用不明确的错误。



您可能会争辩说,因为我们知道 T 的类型边界为 B ,这是一个类,而不是一个功能性接口, lambda不可能适用于这种重载,因此应该在重载解析期间排除lambda,消除模糊性。我不是那个有争论的人。 :-)这可能确实是编译器或甚至规范中的一个bug。



我知道这个领域在设计过程中经历了一系列的变化早期的变化试图将更多的类型检查和推理信息引入重载解决阶段,但是它们很难实现,指定和理解。 (是的,甚至比现在更难理解。)不幸的是问题不断出现。决定通过减少可能超载的范围来简化事情。


类型推断和重载是反对的;从第1天开始,许多类型推断的语言都禁止重载(除了可能在arity上。)因此,对于需要推理的隐式lambdas这样的构造,似乎合理的是放弃一些重载力的东西来增加可以使用隐式lambdas的情况的范围。


- 布赖恩戈茨,Lambda专家组,2013年8月9日

(这是一个相当有争议的决定。在这个线程中有116条消息,并且还有其他几个线程讨论这个问题。)

这个决定的一个后果是某些API必须更改为避免重载,例如,比较器API 。以前, Comparator.comparing 方法有四个重载:

 比较函数)
比较(ToDoubleFunction)
比较(ToIntFunction)
比较(ToLongFunction)

问题是这些重载只能通过lambda返回类型来区分,我们实际上从来没有得到类型推断在隐式类型lambdas中工作。为了使用这些,总是必须为lambda转换或提供显式类型参数。这些API后来改为:

 比较(函数)
比较双(ToDoubleFunction)
比较函数(ToIntFunction )
比较长(ToLongFunction)

这有点笨拙,但它完全没有歧义。类似的情况发生在 Stream.map mapToDouble mapToInt mapToLong ,以及API的其他地方。



最重要的是,在类型推断存在的情况下,分辨率的权利通常是非常困难的,并且语言和编译器设计人员为了使类型推断更好地工作而从超负荷分辨率中剥离了权力。出于这个原因,Java 8 API避免了预计会使用隐式类型lambda的重载方法。


I'm playing around with java8 lambdas and I came across a compiler error which I didn't expect.

Say I have a functional interface A, an abstract class B and a class C with overloaded methods that take either A or B as arguments:

public interface A { 
  void invoke(String arg); 
}

public abstract class B { 
  public abstract void invoke(String arg); 
}

public class C {
  public void apply(A x) { }    
  public B apply(B x) { return x; }
}

Then I can pass a lambda into c.apply and it is correctly resolved to c.apply(A).

C c = new C();
c.apply(x -> System.out.println(x));

But when I change the overload that takes B as argument to a generic version the compiler reports that the two overloads are ambiguous.

public class C {
  public void apply(A x) { }    
  public <T extends B> T apply(T x) { return x; }
}

I thought the compiler would see that T has to be a subclass of B which is not a functional interface. Why can't it resolve the correct method?

解决方案

There is a lot of complexity at the intersection of overload resolution and type inference. The current draft of the lambda specification has all the gory details. Sections F and G cover overload resolution and type inference, respectively. I don't pretend to understand it all. The summary sections in the introduction are fairly understandable, though, and I recommend that people read them, particularly the summaries of sections F and G, to get an idea of what's going on in this area.

To recap the issues briefly, consider a method call with some arguments in the presence of overloaded methods. Overload resolution has to choose the right method to call. The "shape" of the method (arity, or number of arguments) is most significant; obviously a method call with one argument can't resolve to a method that takes two parameters. But overloaded methods often have the same number of parameters of different types. In this case, the types start to matter.

Suppose there are two overloaded methods:

    void foo(int i);
    void foo(String s);

and some code has the following method call:

    foo("hello");

Obviously this resolves to the second method, based on the type of the argument being passed. But what if we are doing overload resolution, and the argument is a lambda? (Especially one whose types are implicit, that relies on type inference to establish the types.) Recall that a lambda expression's type is inferred from the target type, that is, the type expected in this context. Unfortunately, if we have overloaded methods, we don't have a target type until we've resolved which overloaded method we're going to call. But since we don't yet have a type for the lambda expression, we can't use its type to help us during overload resolution.

Let's look at the example here. Consider interface A and abstract class B as defined in the example. We have class C that contains two overloads, and then some code calls the apply method and passes it a lambda:

    public void apply(A a)    
    public B apply(B b)

    c.apply(x -> System.out.println(x));

Both apply overloads have the same number of parameters. The argument is a lambda, which must match a functional interface. A and B are actual types, so it's manifest that A is a functional interface whereas B is not, therefore the result of overload resolution is apply(A). At this point we now have a target type A for the lambda, and type inference for x proceeds.

Now the variation:

    public void apply(A a)    
    public <T extends B> T apply(T t)

    c.apply(x -> System.out.println(x));

Instead of an actual type, the second overload of apply is a generic type variable T. We haven't done type inference, so we don't take T into account, at least not until after overload resolution has completed. Thus both overloads are still applicable, neither is most specific, and the compiler emits an error that the call is ambiguous.

You might argue that, since we know that T has a type bound of B, which is a class, not a functional interface, the lambda can't possibly apply to this overload, thus it should be ruled out during overload resolution, removing the ambiguity. I'm not the one to have that argument with. :-) This might indeed be a bug in either the compiler or perhaps even in the specification.

I do know that this area went through a bunch of changes during the design of Java 8. Earlier variations did attempt to bring more type checking and inference information into the overload resolution phase, but they were harder to implement, specify, and understand. (Yes, even harder to understand than it is now.) Unfortunately problems kept arising. It was decided to simplify things by reducing the range of things that can be overloaded.

Type inference and overloading are ever in opposition; many languages with type inference from day 1 prohibit overloading (except maybe on arity.) So for constructs like implicit lambdas, which require inference, it seems reasonable to give up something in overloading power to increase the range of cases where implicit lambdas can be used.

-- Brian Goetz, Lambda Expert Group, 9 Aug 2013

(This was quite a controversial decision. Note that there were 116 messages in this thread, and there are several other threads that discuss this issue.)

One of the consequences of this decision was that certain APIs had to be changed to avoid overloading, for example, the Comparator API. Previously, the Comparator.comparing method had four overloads:

    comparing(Function)
    comparing(ToDoubleFunction)
    comparing(ToIntFunction)
    comparing(ToLongFunction)

The problem was that these overloads are differentiated only by the lambda return type, and we actually never quite got the type inference to work here with implicitly-typed lambdas. In order to use these one would always have to cast or supply an explicit type argument for the lambda. These APIs were later changed to:

    comparing(Function)
    comparingDouble(ToDoubleFunction)
    comparingInt(ToIntFunction)
    comparingLong(ToLongFunction)

which is somewhat clumsy, but it's entirely unambiguous. A similar situation occurs with Stream.map, mapToDouble, mapToInt, and mapToLong, and in a few other places around the API.

The bottom line is that getting overload resolution right in the presence of type inference is very difficult in general, and that the language and compiler designers traded away power from overload resolution in order to make type inference work better. For this reason, the Java 8 APIs avoid overloaded methods where implicitly typed lambdas are expected to be used.

这篇关于Java8:lambda表达式和重载方法的歧义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆