为啥在编译时不检查 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() 时,您会收到错误。例如&lt;R&gt; R with(Function&lt;T, R&gt; getter, T input, R returnValue) return getter.apply(input); jukzi,也许您应该提供代码或说明您的 with 方法实际上应该做什么以及为什么需要 RInteger。为此,您需要向我们展示您希望如何利用返回值。您似乎想实现某种构建器模式,但我无法识别常见模式或您的意图。 谢谢。我还考虑过检查完整的初始化。但是由于我在编译时看不到任何方法,所以我更喜欢坚持使用默认值 null/0。我也不知道如何在编译时检查非接口方法。在运行时使用像“.with(m -> 1).returning(1)”这样的非接口已经导致早期的java.lang.NoSuchMethodException 【参考方案1】:

在第一个示例中,MyInterface::getLength"I am NOT an Integer" 帮助将泛型参数 TR 分别解析为 MyInterfaceSerializable &amp; Comparable&lt;? extends Serializable &amp; Comparable&lt;?&gt;&gt;

// 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&lt;MyInterface, Integer&gt;,除非您明确说明,否则会导致编译时错误,如第二个示例所示。

// 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).&lt;Integer&gt;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 显式声明为 很有趣,并且完全回答了为什么会出错的问题。但是我仍在寻找解决方案而不明确声明类型。有什么想法吗? @jukzi 您在寻找什么样的解决方案?代码已经编译,如果你想使用它。您正在寻找的一个示例将有助于进一步说明问题。【参考方案3】:

这是因为你的泛型类型参数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 而不是复制初始化时会编译?

intellij IDEA为啥运行时不编译grails或groovy了。

为啥数据库或语言平台在执行查询时不返回强类型类? [关闭]