为什么类型参数比方法参数强tl;博士完整解释有关“扩展示例”的其他说明

2022-09-04 21:19:22

为什么是

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) {...}

这是为什么在编译时不检查 lambda 返回类型的后续内容。我发现使用的方法像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

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

tl;博士

找出这种差异涉及对Java的类型推理规范进行一些相当深入的阅读,但基本上可以归结为:

  • 所有其他条件相同,编译器可以推断出最具体的类型。
  • 但是,如果它可以找到满足所有要求的类型参数的替换,则编译将成功,无论替换结果多么模糊
  • 因为有一个(诚然模糊的)替换满足以下方面的所有要求:withRSerializable
  • 对于 ,附加类型参数的引入会强制编译器首先解析,而不考虑约束 。 解析为(更具体),这意味着推理失败。withXFRF extends Function<T,R>RStringF

最后一个要点是最重要的,但也是最容易挥舞的。我想不出一个更好的简洁的措辞方式,所以如果你想要更多细节,我建议你阅读下面的完整解释。

这是预期的行为吗?

我要在这里四肢着地,说

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

这很重要,因为它告知了以下问题:我是否应该在应用程序设计中依赖此行为?我认为你不应该这样做,因为你不能保证该语言的未来版本将继续以这种方式运行。

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

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

您的用例也不例外。

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

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

因此,从逻辑上讲,我们希望每个类型参数在方法签名中至少出现两次,否则“它不执行任何操作”。 在你只在签名中出现一次,这向我暗示了使用一个类型参数,而不是与该语言的这个功能的意图内联。FwithX

替代实现

以稍微“预期行为”的方式实现这一点的一种方法是将方法拆分为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) {
            // 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
  • scond 方法 () 将 的类型限制为与之前设置的内容兼容。ofvalue

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

最后要注意的是,要使整个事情变得无关紧要:我认为Mockito(特别是它的存根功能)可能基本上已经完成了你试图通过“类型安全通用构建器”实现的目标。也许你可以用它来代替?

完整解释

我将完成 和 的类型推断过程。这很长,所以慢慢来。尽管很长,但我仍然留下了很多细节。您可能希望参考规范以获取更多详细信息(点击链接),以说服自己我是对的(我很可能犯了一个错误)。withwithX

另外,为了简化一些事情,我将使用一个更小的代码示例。主要区别在于它交换为 ,因此游戏中的类型和参数较少。下面是一个完整的代码段,可重现您描述的行为:FunctionSupplier

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
    }

}

让我们依次完成每个方法调用的类型适用性推断和类型推断过程:

with

我们有:

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

初始绑定集 B0 为:

  • R <: Object

所有参数表达式都与适用性相关

因此,适用性推断的初始约束集 C 为:

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

简化为以下的绑定集 B2

  • R <: Object(从 B0)
  • Long <: R(从第一个约束)
  • String <: R(来自第二个约束)

由于这不包含绑定的“false”,并且(我假设)成功(给予)的分辨率,那么调用是适用的。RSerializable

因此,我们继续进行调用类型推断

具有关联输入输出变量的新约束集 C 为:

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

这不包含输入输出变量之间的相互依赖关系,因此可以在一个步骤中减少,并且最终的绑定集B4B2相同。因此,分辨率像以前一样成功,编译器松了一口气!

withX

我们有:

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

初始绑定集 B0 为:

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

只有第二个参数表达式与适用性相关。第一个 () 不是,因为它满足以下条件:TypeInference::getLong

如果 是泛型方法,并且方法调用不提供显式类型参数,则显式类型 lambda 表达式或确切的方法引用表达式,其相应的目标类型(从 的签名派生而来)是 的类型参数。mmm

因此,适用性推断的初始约束集 C 为:

  • "Also not a long" 兼容 R

简化为以下的绑定集 B2

  • R <: Object(从 B0)
  • F <: Supplier<R>(从 B0)
  • String <: R(从约束)

同样,由于这不包含绑定的“false”,并且成功(给予)的解析,那么调用是适用的。RString

再次调用类型推理...

这一次,新的约束集 C 以及关联的输入输出变量为:

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

同样,我们在输入变量和输出变量之间没有相互依赖关系。但是,这次有输入变量(),因此我们必须在尝试减少之前解决此问题。因此,我们从绑定集 B2 开始F

  1. 我们按如下方式确定子集:V

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

    通过 B2 中的第二个边界,的分辨率取决于 ,所以 。FRV := {F, R}

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

    设为未实例化变量的非空子集,使得 i) 对于所有变量,如果依赖于变量的分辨率,则要么有实例化,要么有一些这样的实例化;和 ii) 不存在具有此属性的非空专有子集。{ α1, ..., αn }Vi (1 ≤ i ≤ n)αiββjβ = αj{ α1, ..., αn }

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

  3. 使用第三个绑定 (),我们实例化并将其合并到我们的绑定集中。 现在已解决,并且第二个绑定有效地变为 。String <: RR = StringRF <: Supplier<String>

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

现在这个问题已经解决,我们可以继续使用新的约束进行约简F

  1. TypeInference::getLong 兼容 Supplier<String>
  2. ...减少到 兼容Long String
  3. ...这简化为

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


有关“扩展示例”的其他说明

问题中的扩展示例着眼于上述工作原理未直接涵盖的几个有趣案例:

  • 其中值类型是方法返回类型Integer <: Number)
  • 其中功能接口在推断类型中是逆变的(即 而不是ConsumerSupplier)

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

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

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

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

对于其他 2 个(以及涉及您希望解决的任何其他调用),如果您通过为上述方法之一列出的类型推断过程(即 对于第一个,对于第三个)。您只需要注意一个小小的更改:ConsumerwithwithX

  • 第一个参数 ( ) 的约束将减少到 而不是像 它所做的那样。这在链接的关于减少的文档中进行了描述。t::setNumberConsumer<R>R <: NumberNumber <: RSupplier<R>

我把它留给读者一个练习,让他们通过上面的一个过程,用这一额外的知识,向自己证明为什么一个特定的调用可以编译或不编译。


答案 2

推荐