juk*_*kzi 12 java generics lambda type-inference
Why is
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
Run Code Online (Sandbox Code Playgroud)
more strict then
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
Run Code Online (Sandbox Code Playgroud)
This is a follow up on Why is lambda return type not checked at compile time.
I found using the method withX() like
.withX(MyInterface::getLength, "I am not a Long")
Run Code Online (Sandbox Code Playgroud)
produces the wanted compile time error:
The type of getLength() from the type BuilderExample.MyInterface is long, this is incompatible with the descriptor's return type: String
while using the method with() does not.
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 !!!
}
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
The following example shows the different behaviour of method and type parameter boiled down to a Supplier. In addition it shows the difference to a Consumer behaviour for a type parameter. And it shows it does not make a difference wether it is a Consumer or Supplier for a method parameter.
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 :-)
}
}
Run Code Online (Sandbox Code Playgroud)
use*_*601 12
这是一个非常有趣的问题。恐怕答案很复杂。
找出差异需要对Java的类型推断规范进行相当深入的阅读,但基本上可以归结为:
with存在(公认的含糊不清的)替代,它满足以下方面的所有要求R:SerializablewithX,引入额外的类型参数会F强制编译器R首先进行解析,而不考虑约束F extends Function<T,R>。R解析为(更为具体)String,这意味着F失败的推断。最后一个要点是最重要的,也是最手工的。我想不出一种更好的简洁措辞方式,因此,如果您需要更多详细信息,建议您阅读下面的完整说明。
我要去这里走出去的肢体,并说没有。
我并不是说规范中存在错误,更多的是(在情况下withX)语言设计者举起手来说:“在某些情况下,类型推断变得太难了,所以我们只会失败”。即使编译器的行为withX似乎是您想要的,我仍认为这是当前规范的附带副作用,而不是积极的设计决策。
这很重要,因为它提示了问题:我应该在应用程序设计中依赖此行为吗?我认为您不应该这样做,因为您不能保证该语言的未来版本将继续以这种方式运行。
虽然语言设计人员确实很努力在更新规范/设计/编译器时不破坏现有应用程序,但问题是您要依赖的行为是编译器当前失败的行为(即不是现有应用程序)。语言更新始终将未编译的代码变成编译的代码。例如,可以保证以下代码不在Java 7中编译,但可以在Java 8中编译:
static Runnable x = () -> System.out.println();
Run Code Online (Sandbox Code Playgroud)
您的用例没有什么不同。
我会谨慎使用您的withX方法的另一个原因是F参数本身。通常,在一个一般类型参数方法(即没有出现在返回类型)存在的类型的签名的多个部分结合在一起。这是说:
我不在乎什么T,但是要确保无论我在哪里使用T它都是相同的类型。
从逻辑上讲,然后,我们希望每个类型参数在方法签名中至少出现两次,否则“什么也没做”。F您withX在签名中仅出现一次,这向我建议使用不符合该语言功能意图的类型参数。
一种以稍微“预期的行为”方式实施此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);
}
}
Run Code Online (Sandbox Code Playgroud)
然后可以按以下方式使用它:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Run Code Online (Sandbox Code Playgroud)
它不像您withX那样包含无关的类型参数。通过将方法分解为两个签名,它还可以从类型安全的角度更好地表达您要执行的操作的意图:
With),该类根据方法引用定义类型。of)限制的类型value与您先前设置的兼容。语言的未来版本能够编译此代码的唯一方法是,如果实现了完全的鸭式输入,这似乎不太可能。
最后一点,使整个事情变得无关紧要: 我认为Mockito(尤其是其存根功能)基本上已经可以完成您要使用“类型安全的通用生成器”实现的目标。也许您可以只使用它呢?
我将针对和进行类型推断过程。这很长,所以慢慢来。尽管时间很长,但我仍然遗漏了很多细节。您可能希望参考规范以获取更多详细信息(单击链接),以使自己确信我是对的(我很可能犯了一个错误)。withwithX
另外,为简化起见,我将使用最少的代码示例。主要的区别是,它换出Function了Supplier,所以有较少的类型和游戏参数。这是完整的代码段,可再现您描述的行为:
static Runnable x = () -> System.out.println();
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
初始边界集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 4与B 2相同。因此,解析像以前一样成功,并且编译器松了一口气!
withX我们有:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
Run Code Online (Sandbox Code Playgroud)
初始边界集B 0为:
R <: ObjectF <: 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开始。
我们确定一个子集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解决了,我们可以继续进行还原,使用新的约束:
TypeInference::getLong 与...兼容 Supplier<String>Long 与 String...,然后出现编译器错误!
该问题中的扩展示例研究了一些有趣的案例,而上述工作并未直接涵盖这些案例:
Integer <: NumberConsumer而不是Supplier)特别是,给定的调用中有3个可能会暗示与解释中所描述的行为“不同”的编译器行为:
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
}
}
Run Code Online (Sandbox Code Playgroud)
这些3的第二将经历完全相同的推理过程如withX以上(只需更换Long与Number和String带Integer)。这说明了为什么您不应该在类设计中依赖此失败的类型推断行为的另一个原因,因为在此处无法进行编译可能不是理想的行为。
对于其他2(实际上任何涉及其他调用的Consumer你想通过工作),该行为应该是显而易见的,如果你通过了上述方法中的一个(即奠定了类型推断过程的工作with为先,withX为第三)。您只需要注意一个小变化:
t::setNumber 与兼容 Consumer<R>)将降低至R <: Number,而不是Number <: R因为它为Supplier<R>。有关减少的链接文档对此进行了描述。我将其作为练习的机会,让读者能够熟练地完成上述过程之一,并掌握更多的这些知识,以向自己确切地说明为什么特定的调用会编译或不编译。