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


12

为什么是

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()类型很长,这与描述符的返回类型不兼容:字符串

而使用该方法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
由于与后者的推论。尽管它们都是基于用例的,但仍然需要实现。对于您来说,前者可能是严格而又好的。为了获得灵活性,其他人可以选择后者。
纳曼

您是否正在尝试在Eclipse中进行编译?搜索您粘贴的格式的错误字符串表明这是Eclipse(ecj)特定的错误。使用Raw javac或诸如Gradle或Maven之类的构建工具进行编译时,是否会遇到相同的问题?
user31601 '19

@ user31601我使用javac输出添加了完整示例。错误消息的格式几乎没有什么不同,但是eclipse和javac仍具有相同的行为
jukzi

Answers:


12

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

tl; dr

找出差异需要对Java的类型推断规范进行相当深入的阅读,但基本上可以归结为:

  • 在其他所有条件相同的情况下,编译器会推断出可以的最具体类型。
  • 但是,如果它可以找到满足所有要求的类型参数替代项,则编译将成功,但是替代项的含糊之处却是如此。
  • 因为with有一个(公认的含糊的)替代词,它满足以下方面的所有要求RSerializable
  • 为此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它都是相同的类型。

从逻辑上讲,然后,我们希望每个类型参数在方法签名中至少出现两次,否则“什么也没做”。FwithX在签名中仅出现一次,这向我建议使用不符合该语言功能意图的类型参数。

替代实施

一种以稍微“预期的行为”方式实现此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),该类根据方法引用定义类型。
  • scond方法(of限制的类型value与您先前设置的兼容。

该语言的未来版本能够编译此代码的唯一方法是,如果实现了完全的鸭式输入,这似乎不太可能。

最后一点,使整个事情变得无关紧要: 我认为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");

初始边界集B 0为:

  • R <: Object

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

因此,对于初始约束集适用性推理Ç,是:

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

减少到以下的边界集B 2

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

由于这种不包含绑定“ ”,并且(我认为)分辨率R成功(给Serializable),然后调用适用。

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

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

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

它不包含输入输出变量之间的相互依赖关系,因此可以在一个步骤中减少它,并且最终边界集B 4B 2相同。因此,解析像以前一样成功,并且编译器松了一口气!

withX

我们有:

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

初始边界集B 0为:

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

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

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

因此,对于初始约束集适用性推理Ç,是:

  • "Also not a long" 与...兼容 R

减少到以下的边界集B 2

  • R <: Object(从B 0开始
  • F <: Supplier<R>(从B 0开始
  • String <: R (从约束)

同样,因为这不包含约束“ ”,并且分辨率R成功(给String),然后调用适用。

调用类型推断再一次...

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

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

同样,我们在输入输出变量之间没有相互依赖性。然而这个时候,有一个输入变量F),所以我们必须解决在尝试在此之前减少。因此,我们从边界集B 2开始

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

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

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

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

    { α1, ..., αn }是未初始化的变量的一个非空的子集V,使得i)对于所有i (1 ≤ i ≤ n),如果αi依赖于可变的分辨率β,则要么β具有实例化或有一些j使得β = αj; ii)不存在{ α1, ..., αn }具有此属性的非空适当子集。

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

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

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

现在F解决了,我们可以继续进行还原,使用新的约束:

  1. TypeInference::getLong 与...兼容 Supplier<String>
  2. ...降低Long String
  3. ...减少为

...,然后出现编译器错误!


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

问题中的扩展示例研究了一些有趣的情况,但上面的工作并未直接涵盖这些情况:

  • 其中值类型是方法返回类型()的类型Integer <: 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 :-)

这些3的第二将经历完全相同的推理过程如withX以上(只需更换LongNumberStringInteger)。这说明了为什么您不应该在类设计中依赖此失败的类型推断行为的另一个原因,因为在此处无法进行编译可能不是理想的行为。

对于其他2(实际上任何涉及其他调用的Consumer你想通过工作),该行为应该是显而易见的,如果你通过了上述方法中的一个(即奠定了类型推断过程的工作with为先,withX为第三)。您只需要注意一个小变化:

  • 第一个参数(约束t::setNumber 与兼容 Consumer<R>)将降低R <: Number,而不是Number <: R因为它为Supplier<R>。有关减少的链接文档对此进行了描述。

我将其作为练习的机会,让读者能够熟练地完成上述过程之一,并掌握更多的这些知识,以向自己确切地说明为什么特定的调用会编译或不编译。


非常深入,深入研究和制定。谢谢!
Zabuzard

@ user31601您能否指出供应商与消费者之间的区别在哪里起作用。为此,我在原始问题中添加了扩展示例。它显示了不同版本的letBe(),letBeX()和let()。be()的协变,协变和不变行为,具体取决于供应商/消费者。
jukzi

@jukzi我添加了一些其他说明,但是您应该有足够的信息亲自完成这些新示例。
user31601 '19

有趣的是:18.2.1中有很多特殊情况。对于lambda和方法参考,从我幼稚的理解中我根本不会期望它们有任何特殊情况。而且可能没有普通的开发人员期望。
jukzi

好吧,我想原因是使用lambda和方法引用时,编译器需要确定lambda应该实现哪种正确类型-它必须做出选择!例如,TypeInference::getLong可以实现Supplier<Long>Supplier<Serializable>Supplier<Number>等,但至关重要的是,它只能实现其中之一(就像其他任何类一样)!这与所有其他表达式不同,在所有其他表达式中,已实现的类型都是预先知道的,并且编译器只需要确定它们中的一个是否满足约束条件即可。
user31601 '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.