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

时间:2019-10-14 12:15:35

标签: java generics lambda type-inference

为什么

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()类型很长,与描述符的返回类型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 :-)
  }
}

1 个答案:

答案 0 :(得分:12)

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

tl; dr

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

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

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

这是预期的行为吗?

我要在这里四肢走动,说

我并不是说规范中存在错误,更多的是(在withX情况下,语言设计者举起手来说”“在某些情况下,类型推断会太辛苦了,我们就会失败”。即使编译器相对于withX的行为似乎是您想要的,我仍认为这是当前规范的附带副作用,而不是积极的设计决策。

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

虽然确实是语言设计人员在更新其规范/设计/编译器时尽了最大努力不破坏现有的应用程序,但问题是您要依赖的行为是当前编译器失败的一种行为(即不是现有的应用)。语言更新始终将未编译的代码变成编译的代码。例如,可以保证不在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(尤其是其存根功能)可能已经基本完成了您要实现的目标您的“类型安全的通用生成器”。也许您可以改用它?

完整的说明

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

此外,为简化起见,我将使用更简单的代码示例。主要区别在于它用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 inferencetype inference类型的过程:

with

我们有:

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

初始绑定集 B 0 是:

  • R <: Object

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

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

  • TypeInference::getLong Supplier<R>
  • 兼容
  • "Not a long" R
  • 兼容

reduces绑定以下项的集合 B 2

  • R <: Object(来自 B 0
  • Long <: R(从第一个约束开始)
  • String <: R(来自第二个约束)

因为它不包含绑定的' false ',并且(我假设)R的{​​{3}}成功(给出Serializable),然后调用适用。

因此,我们进入resolution

具有新的 input output 变量的新约束集 C 为:

  • TypeInference::getLong Supplier<R>兼容
    • 输入变量:
    • 输出变量:R

这不包含 input output 变量之间的相互依赖性,因此可以invocation type inference一步完成,最后绑定集 B 4 ,与 B 2 相同。因此,reduced像以前一样成功,并且编译器松了一口气!

withX

我们有:

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

初始绑定集 B 0 是:

  • R <: Object
  • F <: Supplier<R>

仅第二个参数表达式为resolution。第一个(TypeInference::getLong)不是,因为它满足以下条件:

  

如果m是通用方法,并且方法调用不提供显式类型参数,显式类型的lambda表达式或精确方法引用表达式,则其对应的目标类型(从{{ 1}})是m的类型参数。

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

  • m "Also not a long"
  • 兼容

applicability inference绑定以下项的集合 B 2

  • R(来自 B 0
  • R <: Object(来自 B 0
  • F <: Supplier<R>(来自约束)

同样,由于它不包含绑定的' false ',并且String <: R中的reduces成功(给出了R),因此该调用是适用的

resolution再次...

这一次,新的约束集 C 和相关的 input output 变量为:

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

同样,我们在 input output 变量之间没有相互依赖性。但是这一次,有一个输入变量(F),因此我们必须Invocation type inference,然后再尝试resolve。因此,我们从绑定集 B 2 开始。

  1. 我们按如下确定子集F

      

    给出一组要解析的推理变量,令V是该集合与该集合中至少一个变量的分辨率所依赖的所有变量的并集。

    B 2 中的第二个边界,V的分辨率取决于F,因此R

  2. 我们根据规则选择V := {F, R}的子集:

      

    V{ α1, ..., αn }中未实例化变量的非空子集,这样,如果V取决于变量{的分辨率,则i)对于所有i (1 ≤ i ≤ n) {1}},则αi具有实例化或存在一些β使得β; ii)不存在具有此属性的j的非空正确子集。

    β = αj满足此属性的唯一子集是{ α1, ..., αn }

  3. 使用第三个界限(V)实例化{R}并将其合并到我们的界限集中。 String <: R现在已解决,第二个边界有效地变为R = String

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

现在F = Supplier<String>已解决,我们可以使用新的约束条件继续进行reduction

  1. F F
  2. 兼容
  3. ...简化为TypeInference::getLong Supplier<String>
  4. 兼容
  5. ...减少为 false

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


“扩展示例”的其他注释

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

  • 值类型是方法返回类型(Long)的子类型
  • 其中功能接口与推断类型相反(即String而不是Integer <: Number

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

Consumer

这3个中的第二个将通过与上述Supplier完全相同的推理过程(只需将t.lettBe(t::setNumber, "NaN"); // Does not compile :-) t.letBeX(t::getNumber, 2); // !!! Does not compile :-( t.lettBeX(t::setNumber, 2); // Compiles :-) 替换为withX,将Long替换为Number) 。这说明了为什么您不应该在类设计中依赖于这种失败的类型推断行为的另一个原因,因为无法在此处进行编译可能 不是理想的行为。

对于其他2个(以及您实际上希望进行的涉及String的其他任何调用),如果您通过上述方法之一设计的类型推断过程,则该行为应显而易见。 (即,Integer代表第一个,Consumer代表第三个)。您只需注意一个小变化:

  • 对第一个参数(with withX兼容)的约束将从reduction变为t::setNumber,而不是Consumer<R>就像R <: Number一样。有关减少的链接文档对此进行了说明。

我将其作为练习的目的,是读者可以借助上面的这些知识来熟练地完成上述过程之一,以向自己确切地说明为什么特定的调用会编译或不编译。