为什么类型参数比方法参数更强

问题描述 投票:6回答:1

为什么是

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

然后更严格

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

这是对Why is lambda return type not checked at compile time的跟进。我发现使用方法withX(),例如

.withX(MyInterface::getLength, "I am not a Long")

产生所需的编译时错误:

来自BuilderExample.MyInterface类型的getLength()类型很长,这与描述符的返回类型不兼容:字符串

使用方法with()时不。

完整示例:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors
java generics lambda type-inference
1个回答
1
投票

这是一个非常有趣的问题。恐怕答案很复杂。

tl; dr

找出差异需要对Java的type inference specification进行相当深入的阅读,但基本上可以归结为:

  • 在所有其他条件相同的情况下,编译器会推断出可以的最具体
  • 类型。
  • 但是,如果可以找到满足所有要求的类型参数的a替代,则编译将成功,但是vague
  • 替代实际上是。
  • 对于with,有一个(公认的含糊的)替代词,它满足[C​​0]的所有要求:R
  • 对于Serializable,引入附加类型参数withX会强制编译器首先解析F,而不考虑约束RF extends Function<T,R>解析为(更具体的)R,这意味着String的推理失败。

最后一个要点是最重要的,也是最手工的。我想不出一种更好的简洁措辞方式,因此,如果您需要更多详细信息,建议您阅读下面的完整说明。

这是预期的行为吗?

我要在这里四肢走动,说no

我并不是说规范中存在错误,更多的是(在F的情况下)语言设计者举起手来说“在某些情况下,类型推断变得太难了,所以我们只会失败“

。即使编译器相对于withX的行为似乎是您想要的,我仍认为这是当前规范的附带副作用,而不是积极的设计决策。

这很重要,因为它告知了问题我应该在我的应用程序设计中依赖此行为吗?

我认为您不应该这样做,因为您不能保证该语言的未来版本将继续起作用这样。

虽然确实是语言设计人员在更新其规范/设计/编译器时尽最大努力不破坏现有应用程序,但问题是您要依赖的行为是编译器当前的行为[[失败(即不是现有应用程序)。语言更新始终将未编译的代码变成编译的代码。例如,以下代码可能不是[[保证在Java 7中编译,而是

在Java 8中编译:withX您的用例没有什么不同。
[我对使用static Runnable x = () -> System.out.println();
方法要谨慎的另一个原因是withX参数本身。通常,存在

method

上的通用类型参数(不会出现在返回类型中)以将签名的多个部分的类型绑定在一起。意思是:

我不在乎F是什么,但要确保无论我在哪里使用T都是同一类型。

逻辑上,我们希望每个类型参数在方法签名中至少出现两次,否则“什么也没做”。 T中的F在签名中仅出现一次,这向我建议使用类型参数,该参数不与该语言功能的intent

内联。

替代实现

一种以稍微更多的“预期行为”方式实现此目的的方法是将withX方法分成2个链:

with

然后可以按如下方式使用:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

[这不像b.with(MyInterface::getLong).of(1L); // Compiles b.with(MyInterface::getLong).of("Not a long"); // Compiler error 那样包含无关的类型参数。通过将方法分解为两个签名,它还可以从类型安全的角度更好地表达您要执行的操作的意图:

第一个方法根据方法引用建立一个类(withX),该类

    定义类型。
  • 第二种方法(With约束
  • of的类型要与您先前设置的兼容。
  • [该语言的未来版本唯一能够编译的方法是如果实现了完全的鸭式输入,这似乎不太可能。

    使整件事无关紧要的最后注解:

    我认为value(尤其是其存根功能)可能已经基本完成了您要使用“类型安全的通用构建器”实现的目标。也许您可以改用它?

    完整说明我将同时通过Mockitotype inference procedurewith工作。这很长,所以慢慢来。尽管时间很长,但我仍然遗漏了很多细节。您可能希望参考规范以获取更多详细信息(单击链接),以使自己确信我是对的(我很可能犯了一个错误)。

    此外,为了简化一些事情,我将使用一个更小的代码示例。主要区别在于它用withX换成Function,因此播放的类型和参数更少。这是完整的代码段,可再现您描述的行为:

    Supplier

    让我们依次为每个方法调用完成public class TypeInference { static long getLong() { return 1L; } static <R> void with(Supplier<R> supplier, R value) {} static <R, F extends Supplier<R>> void withX(F supplier, R value) {} public static void main(String[] args) { with(TypeInference::getLong, "Not a long"); // Compiles withX(TypeInference::getLong, "Also not a long"); // Does not compile } } applicability inference类型的过程:

    type inference

    我们有:

    with

    初始绑定集

    B 

    0

    是:
  • with(TypeInference::getLong, "Not a long");
      所有参数表达式均为R <: Object
  • 因此,为pertinent to applicability设置的初始约束

    C

    是:

    applicability inference

      与...兼容

  • TypeInference::getLong
  • Supplier<R> 与...兼容
  • "Not a long"
  • R绑定到的集合B

    2

    的其中:reduces(来自
      B
    • 0
  • R <: Object(来自第一个约束)Long <: R(来自第二个约束)
  • 因为它不包含绑定的'
  • false',并且(我假设)String <: Rresolution成功(给出R),所以该调用是适用的。

    所以,我们继续到Serializable新的约束集

    C

    以及关联的

    input和output

    变量是:invocation type inference
      与...兼容
    TypeInference::getLong
  • 输入变量:
  • 输出变量:Supplier<R>
  • 这不包含
  • input
  • output变量之间的相互依赖关系,因此可以在单个步骤中将其设为R,而最后的边界集B 4是与B 2

    相同。因此,reduced像以前一样成功,并且编译器松了一口气!resolution我们有:

    withX

    初始绑定集

    B 

    0

    是:withX(TypeInference::getLong, "Also not a long");
      R <: Object
  • 仅第二个参数表达式为F <: Supplier<R>。第一个(pertinent to applicability)不是,因为它满足以下条件:
  • 如果TypeInference::getLong是通用方法,并且方法调用不提供显式类型参数,显式类型的lambda表达式或精确方法引用表达式,则对应的目标类型(从m的签名派生)是类型参数m

  • 因此,为m设置的初始约束

    C

    是:

    applicability inference

      与...兼容 "Also not a long"
  • R绑定到的集合B

    2

  • 的其中:reduces(来自
      B
    • 0
    R <: Object(来自B
  • 0
  • F <: Supplier<R>(来自约束)同样,由于它不包含绑定的'
  • false',并且String <: Rresolution成功(给出R),因此该调用是适用的。

    String再一次...这次,新的约束集

    C

    与相关的[[input和

    output

    变量是:Invocation type inference 与...兼容
  • TypeInference::getLong
    • 输入变量:F输出变量:
  • 同样,我们在
  • input和output变量之间没有相互依赖性。但是这一次,有一个

    输入变量

    F),因此我们在尝试F之前必须先将此resolve设置为reduction。因此,我们从绑定集B 2
  • 开始。我们如下确定子集V
      给出一组要解析的推理变量,使V为该集合与该集合中至少一个变量的分辨率所依赖的所有变量的并集。
  • 通过

    B

    2的第二个边界,F的分辨率取决于R,因此V := {F, R}
  • [我们根据规则选择V的子集:

    { α1, ..., αn }V中未实例化变量的非空子集,这样i)对于所有i (1 ≤ i ≤ n),如果αi取决于变量β的分辨率,则β具有实例化或存在一些j使得β = αj; ii)不存在具有此属性的{ α1, ..., αn }的非空正确子集。
  • 满足此属性的V的唯一子集是{R}
  • 使用第三个边界(String <: R),我们实例化R = String并将其合并到我们的边界集中。现在解析R,第二个边界有效地变为F <: Supplier<String>

    使用(修订的)第二个边界,我们实例化F = Supplier<String>F现在已解决。

  • 现在解决了F,我们可以使用新的约束条件继续进行reduction

  • TypeInference::getLong

    与...兼容

  • Supplier<String>

    ...简化为Long

      与]兼容> String
    1. ...减少为false
    2. ...我们会收到编译器错误!
    © www.soinside.com 2019 - 2024. All rights reserved.