为啥在编译时不检查 lambda 返回类型?
Posted
技术标签:
【中文标题】为啥在编译时不检查 lambda 返回类型?【英文标题】:Why is lambda return type not checked at compile time?为什么在编译时不检查 lambda 返回类型? 【发布时间】:2020-02-08 18:26:07 【问题描述】:使用的方法引用的返回类型为Integer
。但是在以下示例中允许使用不兼容的String
。
如何修复方法with
声明以在不手动转换的情况下获得方法引用类型安全?
import java.util.function.Function;
public class MinimalExample
static public class Builder<T>
final Class<T> clazz;
Builder(Class<T> clazz)
this.clazz = clazz;
static <T> Builder<T> of(Class<T> clazz)
return new Builder<T>(clazz);
<R> Builder<T> with(Function<T, R> getter, R returnValue)
return null; //TODO
static public interface MyInterface
Integer getLength();
public static void main(String[] args)
// missing compiletimecheck is inaceptable:
Builder.of(MyInterface.class).with(MyInterface::getLength, "I am NOT an Integer");
// compile time error OK:
Builder.of(MyInterface.class).with((Function<MyInterface, Integer> )MyInterface::getLength, "I am NOT an Integer");
// The method with(Function<MinimalExample.MyInterface,R>, R) in the type MinimalExample.Builder<MinimalExample.MyInterface> is not applicable for the arguments (Function<MinimalExample.MyInterface,Integer>, String)
用例:类型安全但通用的生成器。
我尝试实现一个没有注释处理(自动值)或编译器插件(lombok)的通用构建器
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public class BuilderExample
static public class Builder<T> implements InvocationHandler
final Class<T> clazz;
HashMap<Method, Object> methodReturnValues = new HashMap<>();
Builder(Class<T> clazz)
this.clazz = clazz;
static <T> Builder<T> of(Class<T> clazz)
return new Builder<T>(clazz);
Builder<T> withMethod(Method method, Object returnValue)
Class<?> returnType = method.getReturnType();
if (returnType.isPrimitive())
if (returnValue == null)
throw new IllegalArgumentException("Primitive value cannot be null:" + method);
else
try
boolean isConvertable = getDefaultValue(returnType).getClass().isAssignableFrom(returnValue.getClass());
if (!isConvertable)
throw new ClassCastException(returnValue.getClass() + " cannot be cast to " + returnType + " for " + method);
catch (IllegalArgumentException | SecurityException e)
throw new RuntimeException(e);
else if (returnValue != null && !returnType.isAssignableFrom(returnValue.getClass()))
throw new ClassCastException(returnValue.getClass() + " cannot be cast to " + returnType + " for " + method);
Object previuos = methodReturnValues.put(method, returnValue);
if (previuos != null)
throw new IllegalArgumentException("Value alread set for " + method);
return this;
static HashMap<Class, Object> defaultValues = new HashMap<>();
private static <T> T getDefaultValue(Class<T> clazz)
if (clazz == null || !clazz.isPrimitive())
return null;
@SuppressWarnings("unchecked")
T cachedDefaultValue = (T) defaultValues.get(clazz);
if (cachedDefaultValue != null)
return cachedDefaultValue;
@SuppressWarnings("unchecked")
T defaultValue = (T) Array.get(Array.newInstance(clazz, 1), 0);
defaultValues.put(clazz, defaultValue);
return defaultValue;
public synchronized static <T> Method getMethod(Class<T> clazz, java.util.function.Function<T, ?> resolve)
AtomicReference<Method> methodReference = new AtomicReference<>();
@SuppressWarnings("unchecked")
T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] clazz , new InvocationHandler()
@Override
public Object invoke(Object p, Method method, Object[] args)
Method oldMethod = methodReference.getAndSet(method);
if (oldMethod != null)
throw new IllegalArgumentException("Method was already called " + oldMethod);
Class<?> returnType = method.getReturnType();
return getDefaultValue(returnType);
);
resolve.apply(proxy);
Method method = methodReference.get();
if (method == null)
throw new RuntimeException(new NoSuchMethodException());
return method;
// R will accep common type Object :-( // see https://***.com/questions/58337639
<R, V extends R> Builder<T> with(Function<T, R> getter, V returnValue)
Method method = getMethod(clazz, getter);
return withMethod(method, returnValue);
//typesafe :-) but i dont want to avoid implementing all types
Builder<T> withValue(Function<T, Long> getter, long returnValue)
return with(getter, returnValue);
Builder<T> withValue(Function<T, String> getter, String returnValue)
return with(getter, returnValue);
T build()
@SuppressWarnings("unchecked")
T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] clazz , this);
return proxy;
@Override
public Object invoke(Object proxy, Method method, Object[] args)
Object returnValue = methodReturnValues.get(method);
if (returnValue == null)
Class<?> returnType = method.getReturnType();
return getDefaultValue(returnType);
return returnValue;
static public interface MyInterface
String getName();
long getLength();
Long getNullLength();
Long getFullLength();
Number getNumber();
public static void main(String[] args)
MyInterface x = Builder.of(MyInterface.class).with(MyInterface::getName, "1").with(MyInterface::getLength, 1L).with(MyInterface::getNullLength, null).with(MyInterface::getFullLength, new Long(2)).with(MyInterface::getNumber, 3L).build();
System.out.println("name:" + x.getName());
System.out.println("length:" + x.getLength());
System.out.println("nullLength:" + x.getNullLength());
System.out.println("fullLength:" + x.getFullLength());
System.out.println("number:" + x.getNumber());
// java.lang.ClassCastException: class java.lang.String cannot be cast to long:
// RuntimeException only :-(
MyInterface y = Builder.of(MyInterface.class).with(MyInterface::getLength, "NOT A NUMBER").build();
// java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
// RuntimeException only :-(
System.out.println("length:" + y.getLength());
【问题讨论】:
令人惊讶的行为。出于兴趣:当您使用class
而不是interface
作为构建器时是否相同?
为什么这是不可接受的?第一种情况,你没有给出getLength
的类型,所以可以调整返回Object
(或Serializable
)来匹配String参数。
我可能弄错了,但我认为您的方法 with
是问题的一部分,因为它返回 null
。当通过实际使用函数的 R
类型与参数中的相同 R
来实现方法 with()
时,您会收到错误。例如<R> R with(Function<T, R> getter, T input, R returnValue) return getter.apply(input);
jukzi,也许您应该提供代码或说明您的 with 方法实际上应该做什么以及为什么需要 R
为 Integer
。为此,您需要向我们展示您希望如何利用返回值。您似乎想实现某种构建器模式,但我无法识别常见模式或您的意图。
谢谢。我还考虑过检查完整的初始化。但是由于我在编译时看不到任何方法,所以我更喜欢坚持使用默认值 null/0。我也不知道如何在编译时检查非接口方法。在运行时使用像“.with(m -> 1).returning(1)”这样的非接口已经导致早期的java.lang.NoSuchMethodException
【参考方案1】:
在第一个示例中,MyInterface::getLength
和 "I am NOT an Integer"
帮助将泛型参数 T
和 R
分别解析为 MyInterface
和 Serializable & Comparable<? extends Serializable & Comparable<?>>
。
// it compiles since String is a Serializable
Function<MyInterface, Serializable> function = MyInterface::getLength;
Builder.of(MyInterface.class).with(function, "I am NOT an Integer");
MyInterface::getLength
并不总是Function<MyInterface, Integer>
,除非您明确说明,否则会导致编译时错误,如第二个示例所示。
// it doesn't compile since String isn't an Integer
Function<MyInterface, Integer> function = MyInterface::getLength;
Builder.of(MyInterface.class).with(function, "I am NOT an Integer");
【讨论】:
这个答案完全回答了为什么它被解释为其他意图的问题。有趣的。听起来R是没用的。您知道问题的任何解决方案吗? @jukzi (1) 显式定义方法类型参数(此处为R
):Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");
使其无法编译,或者 (2) 让它隐式解析,并希望在不编译的情况下继续-时间错误【参考方案2】:
类型推断在这里发挥了作用。考虑方法签名中的通用R
:
<R> Builder<T> with(Function<T, R> getter, R returnValue)
如所列情况:
Builder.of(MyInterface.class).with(MyInterface::getLength, "I am NOT an Integer");
R
的类型被成功推断为
Serializable, Comparable<? extends Serializable & Comparable<?>>
而String
确实暗示了这种类型,因此编译成功。
要明确指定R
的类型并找出不兼容的地方,只需将代码行更改为:
Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");
【讨论】:
将 R 显式声明为这是因为你的泛型类型参数R
可以推断为Object,即如下编译:
Builder.of(MyInterface.class).with((Function<MyInterface, Object>) MyInterface::getLength, "I am NOT an Integer");
【讨论】:
没错,如果 OP 将方法的结果分配给Integer
类型的变量,那将是编译错误发生的地方。
@sepp2k 除了Builder
仅在T
中通用,而在R
中不通用。就构建器的类型检查而言,这个Integer
只是被忽略了。
R
被推断为Object
...不是真的
@Thilo 你是对的,当然。我假设with
的返回类型将使用R
。当然,这意味着没有有意义的方式以实际使用参数的方式实际实现该方法。
Naman,你是对的,你和安德鲁用正确的推断类型更详细地回答了它。我只是想给出一个更简单的解释(尽管看这个问题的人可能知道类型推断和其他类型,而不仅仅是Object
)。【参考方案4】:
此答案基于其他答案,这些答案解释了为什么它不能按预期工作。
解决方案
以下代码通过将双函数“with”拆分为两个流畅的函数“with”和“returning”来解决问题:
class Builder<T>
...
class BuilderMethod<R>
final Function<T, R> getter;
BuilderMethod(Function<T, R> getter)
this.getter = getter;
Builder<T> returning(R returnValue)
return Builder.this.with(getter, returnValue);
<R> BuilderMethod<R> with(Function<T, R> getter)
return new BuilderMethod<>(getter);
...
MyInterface z = Builder.of(MyInterface.class).with(MyInterface::getLength).returning(1L).with(MyInterface::getNullLength).returning(null).build();
System.out.println("length:" + z.getLength());
// YIPPIE COMPILATION ERRROR:
// The method returning(Long) in the type BuilderExample.Builder<BuilderExample.MyInterface>.BuilderMethod<Long> is not applicable for the arguments (String)
MyInterface zz = Builder.of(MyInterface.class).with(MyInterface::getLength).returning("NOT A NUMBER").build();
System.out.println("length:" + zz.getLength());
(有点陌生)
【讨论】:
另见***.com/questions/58376589 直接解决方案以上是关于为啥在编译时不检查 lambda 返回类型?的主要内容,如果未能解决你的问题,请参考以下文章
为啥将 lambda 传递给受约束的类型模板参数会导致“不完整类型”编译器错误?
为啥 rand() 编译时不包含 cstdlib 或使用命名空间 std?
为啥我的 Linux 编译的二进制文件在 Windows 上运行时不起作用?
为啥在直接初始化和赋值中传递 lambda 而不是复制初始化时会编译?