为啥类型参数比方法参数强

Posted

技术标签:

【中文标题】为啥类型参数比方法参数强【英文标题】:Why is a type parameter stronger then a method parameter为什么类型参数比方法参数强 【发布时间】:2020-02-11 01:36:52 【问题描述】:

为什么

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() 类型为 long,这与描述符的返回类型不兼容:String

虽然使用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

扩展示例

以下示例显示了归结为供应商的方法和类型参数的不同行为。此外,它还显示了类型参数与消费者行为的差异。它表明,对于方法参数,它是消费者还是供应商都没有区别。

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference 

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> 
    TypeInference be(R r);
  

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) 
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  

【问题讨论】:

因为与后者的推断。尽管它们都基于需要实现的用例。对你来说,前者可能是严格而好的。为了灵活性,其他人可能更喜欢后者。 您是否尝试在 Eclipse 中编译它?搜索您粘贴的格式的错误字符串表明这是一个 Eclipse (ecj) 特定错误。使用原始 javac 或 Gradle 或 Maven 等构建工具进行编译时,您是否遇到同样的问题? @user31601 我添加了一个带有 javac 输出的完整示例。错误消息的格式略有不同,但 eclipse 和 javac 仍然具有相同的行为 【参考方案1】:

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

tl;博士

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

在所有其他条件相同的情况下,编译器会推断出它可以最具体的类型。 但是,如果它可以找到满足所有要求的类型参数的 a 替换,那么编译将成功,但是 模糊 替换结果是。 对于with,有一个(诚然是模糊的)替代满足R 的所有要求:Serializable 对于withX,附加类型参数F 的引入强制编译器首先解析R,而不考虑约束F extends Function&lt;T,R&gt;R 解析为(更具体的)String,这意味着F 的推断失败。

最后一个要点是最重要的,但也是最随意的。我想不出更简洁的措辞方式,所以如果您想了解更多详细信息,建议您阅读下面的完整说明。

这是预期的行为吗?

我会在这里拼命出去,说

我并不是说规范中存在错误,更多的是(在 withX 的情况下)语言设计人员已经举手并说 “在某些情况下类型推断变得太难了,所以我们只会失败”。尽管编译器对withX 的行为似乎是您想要的,但我认为这是当前规范的附带副作用,而不是积极的设计决策。

这很重要,因为它提出了一个问题我应该在我的应用程序设计中依赖这种行为吗?我认为你不应该,因为你不能保证未来版本的语言将继续以这种方式行事。

虽然语言设计者在更新规范/设计/编译器时确实会努力不破坏现有应用程序,但问题是您想要依赖的行为是编译器当前失败的行为 em>(即不是现有的应用程序)。语言更新始终将非编译代码转换为编译代码。例如,以下代码可能保证不会在 Java 7 中编译,但在 Java 8 中编译:

static Runnable x = () -> System.out.println();

您的用例也不例外。

我对使用withX 方法持谨慎态度的另一个原因是F 参数本身。通常,方法(不出现在返回类型中)上的泛型类型参数用于将签名的多个部分的类型绑定在一起。它的意思是:

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

那么,从逻辑上讲,我们希望每个类型参数在方法签名中至少出现两次,否则“它什么也不做”。 F 在您的 withX 中仅在签名中出现一次,这表明我使用了不符合语言此功能的 intent 的类型参数。

另一种实现方式

以稍微“预期行为”的方式实现此功能的一种方法是将您的 with 方法拆分为 2 个链:

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) 
            // TODO: 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 的类型与您之前设置的兼容。

该语言的未来版本能够编译它的唯一方法是实现完整的鸭子类型,这似乎不太可能。

最后一点让整个事情变得无关紧要: 我认为Mockito(尤其是它的存根功能)可能基本上已经完成了你想要用你的“类型安全的通用生成器”。也许你可以改用它?

完整的(ish)解释

我将为withwithX 处理type inference procedure。这很长,所以慢慢来。尽管很长,但我仍然遗漏了很多细节。您可能希望参考规范以获取更多详细信息(点击链接),以说服自己我是对的(我很可能犯了一个错误)。

另外,为了稍微简化一下,我将使用更简洁的代码示例。主要区别在于它将Function 替换为Supplier,因此可以使用的类型和参数更少。这是一个完整的 sn-p,它重现了您描述的行为:

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 inferencetype inference 类型过程:

with

我们有:

with(TypeInference::getLong, "Not a long");

初始绑定集B0是:

R &lt;: Object

所有参数表达式均为pertinent to applicability

因此,applicability inference 的初始约束集 C 为:

TypeInference::getLong 兼容 Supplier&lt;R&gt; "Not a long" 兼容 R

这个reduces绑定集合B2的:

R &lt;: Object(来自B0Long &lt;: R(来自第一个约束) String &lt;: R(来自第二个约束)

由于这不包含绑定的'false',并且(我假设)Rresolution 成功(给出Serializable),那么调用是适用的。

所以,我们转到invocation type inference

新的约束集,C,以及相关的 inputoutput 变量是:

TypeInference::getLong 兼容 Supplier&lt;R&gt; 输入变量: 输出变量:R

这不包含 inputoutput 变量之间的相互依赖关系,因此可以在单个步骤中为 reduced,最终绑定集 B4,与B2相同。至此,resolution 依旧成功,编译器松了一口气!

withX

我们有:

withX(TypeInference::getLong, "Also not a long");

初始绑定集B0是:

R &lt;: Object F &lt;: Supplier&lt;R&gt;

只有第二个参数表达式是pertinent to applicability。第一个(TypeInference::getLong)不是,因为它满足以下条件:

如果m 是泛型方法并且方法调用不提供显式类型参数、显式类型化的 lambda 表达式或对应目标类型的精确方法引用表达式(从 m 的签名派生)是m的类型参数。

因此,applicability inference 的初始约束集 C 为:

"Also not a long" 兼容 R

这个reduces绑定集合B2的:

R &lt;: Object(来自B0F &lt;: Supplier&lt;R&gt;(来自B0String &lt;: R(来自约束)

同样,由于这不包含绑定的'false',并且Rresolution 成功(给出String),那么调用是适用的。

Invocation type inference 再次...

这一次,新的约束集 C 以及相关的 inputoutput 变量是:

TypeInference::getLong 兼容 F 输入变量:F 输出变量:

同样,我们在 inputoutput 变量之间没有相互依赖关系。然而这一次,一个输入变量F),所以我们必须先resolve,然后再尝试reduction。因此,我们从绑定集 B2 开始。

    我们确定一个子集V 如下:

    给定一组要解析的推理变量,让V 成为该集合和该集合中至少一个变量的解析所依赖的所有变量的并集。

    通过B2中的第二个界限,F的分辨率取决于R,所以V := F, R

    我们根据规则选择V 的一个子集:

    α1, ..., αn 成为V 中未实例化变量的非空子集,这样i) 对于所有i (1 ≤ i ≤ n),如果αi 取决于变量β 的分辨率,那么β有一个实例化或者有一些j 这样β = αj;并且 ii) 不存在具有此属性的 α1, ..., αn 的非空真子集。

    满足此属性的V 的唯一子集是R

    使用第三个界限 (String &lt;: R) 我们实例化 R = String 并将其合并到我们的界限集中。 R 现在已解决,第二个边界实际上变为 F &lt;: Supplier&lt;String&gt;

    使用(修改后的)第二个界限,我们实例化F = Supplier&lt;String&gt;F 现已解决。

现在F 已解决,我们可以继续使用reduction,使用新的约束:

    TypeInference::getLong 兼容 Supplier&lt;String&gt; ...减少到Long 兼容 String ...减少到 false

...我们得到一个编译器错误!


关于“扩展示例”的附加说明

问题中的扩展示例着眼于一些有趣的案例,这些案例并未直接包含在上述工作中:

其中值类型是方法返回类型的子类型 (Integer &lt;: Number) 函数接口在推断类型中是逆变的(即Consumer 而不是Supplier

特别是,其中 3 个给定的调用可能表明编译器行为与解释中描述的行为“不同”:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

这三个中的第二个将经历与上述withX 完全相同的推理过程(只需将Long 替换为NumberStringInteger)。这说明了您不应该依赖这种失败的类型推断行为来进行类设计的另一个原因,因为此处编译失败可能不是理想的行为。

对于其他 2 个(以及实际上涉及您希望处理的 Consumer 的任何其他调用),如果您通过为上述方法之一(即with 第一个,withX 第三个)。您只需要注意一个小变化:

第一个参数的约束(t::setNumber 兼容 Consumer&lt;R&gt;)将reduce 转换为R &lt;: Number,而不是像Supplier&lt;R&gt; 那样将Number &lt;: R 转换为Number &lt;: R。这在有关减少的链接文档中进行了描述。

我把它留作练习,让读者仔细完成上述过程之一,借助这一额外知识,向他们自己展示特定调用为什么会编译或不会编译。

【讨论】:

非常深入,经过充分研究和制定。谢谢! @user31601 您能否指出供应商与消费者之间的差异在哪里发挥作用。为此,我在原始问题中添加了一个扩展示例。它显示了不同版本的 letBe()、letBeX() 和 let().be() 的协变、逆变和不变行为,具体取决于供应商/消费者。 @jukzi 我已经添加了一些额外的注释,但是您应该有足够的信息来自己处理这些新示例。 这很有趣:18.2.1 中有这么多特殊情况。对于 lambdas 和方法引用,根据我的天真理解,我根本不会期望它们有任何特殊情况。可能没有普通的开发者会想到。 好吧,我猜原因是对于 lambda 和方法引用,编译器需要决定 lambda 应该实现什么正确的类型——它必须做出选择!例如,TypeInference::getLong 可以实现 Supplier&lt;Long&gt;Supplier&lt;Serializable&gt;Supplier&lt;Number&gt; 等,但至关重要的是它只能实现其中一个(就像任何其他类一样)!这与所有其他表达式不同,其中实现的类型都是预先知道的,编译器只需确定其中一个是否满足约束要求。

以上是关于为啥类型参数比方法参数强的主要内容,如果未能解决你的问题,请参考以下文章

为啥“nvarchar”参数比“文本”“SqlCommand”命令的其他类型更快?

为啥不根据参数类型调用最具体的方法

为啥我们将字符串数组作为参数传递给 main() 方法,为啥不传递任何集合类型或包装类型或原始类型?

java中方法传递参数为啥可以不用基本数据类型

为啥 C# 无法从非泛型静态方法的签名推断泛型类型参数类型?

为啥为非泛型方法或构造函数提供显式类型参数会编译?