为什么在编译时不检查lambda返回类型?


38

使用的方法引用具有返回类型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)
  }

}

用例:类型安全但通用的Builder。

我尝试实现没有注释处理(自动值)或编译器插件(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 /programming/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());
  }

}

1
令人惊讶的行为。出于兴趣:对于构建器使用a class代替a时是否一样interface
GameDroids

为什么那是不可接受的?在第一种情况下,您不提供的类型getLength,因此可以将其调整为返回Object(或Serializable)以匹配String参数。
Thilo

1
我可能会弄错了,但是我认为您的方法with在返回时是问题的一部分null。当with()通过实际使用与参数R相同的函数类型来实现方法时,R会出现错误。例如<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids,

2
jukzi,也许你应该提供代码或关于你用的方法应该实际上做一个解释,为什么你需要RInteger。为此,您需要向我们展示如何利用返回值。似乎您想实现某种构建器模式,但是我无法识别一个通用模式或您的意图。
sfiss

1
谢谢。我还考虑过检查完整的初始化。但是由于我在编译时看不到任何方法,因此我更喜欢使用默认值null / 0。我也不知道如何在编译时检查非接口方法。在运行时使用诸如“ .with(m-> 1).returning(1)”之类的非接口已经导致了早期的java.lang.NoSuchMethodException
jukzi

Answers:


27

在第一个例子,MyInterface::getLength"I am NOT an Integer"帮助解决通用参数T,并RMyInterfaceSerializable & 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::getLengthFunction<MyInterface, Integer>除非您明确声明,否则它并不总是a ,这将导致编译时错误,如第二个示例所示。

// 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

@jukzi(1)显式定义方法类型参数(此处为R):Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");使其不被编译;或者(2)使其隐式地得到解决,并希望不会出现编译时错误
Andrew Tobilko

11

它在这里发挥作用的类型推断。考虑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<?>>

并且a String确实暗示了这种类型,因此编译成功。


要明确指定的类型R并找出不兼容之处,只需将代码行更改为:

Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");

显式地将R声明为<Integer>很有趣,并且可以完全回答为什么它出错的问题。但是,我仍在寻找一种解决方案,而无需明确声明Type。任何的想法?
jukzi

@jukzi您正在寻找哪种解决方案?如果您想使用它,该代码已经编译。您正在寻找的示例可以很好地使事情更清楚。
纳曼

11

这是因为R可以将您的通用类型参数推断为Object,即以下编译:

Builder.of(MyInterface.class).with((Function<MyInterface, Object>) MyInterface::getLength, "I am NOT an Integer");

1
确实,如果OP将方法的结果分配给类型为的变量Integer,那将是发生编译错误的地方。
sepp2k

@ sepp2k除了只是在中Builder是通用的T,而在中不是通用的RInteger就生成器的类型检查而言,这只是被忽略。
Thilo

2
R被推断为Object ...不是真的
纳曼

@Thilo,您当然是对的。我假设with将使用的返回类型R。当然,这意味着没有有意义的方法可以实际使用参数的方式实际实现该方法。
sepp2k

1
纳曼,您说得对,您和安德鲁用正确的推断类型作了更详细的回答。我只是想给出一个简单的解释(尽管任何在看这个问题的人都可能知道类型推断和其他类型而不仅仅是Object)。
sfiss

0

该答案基于其他答案,这些其他原因解释了为什么它无法按预期工作。

以下代码通过将双功能“ 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());

(有点陌生)


另请参见stackoverflow.com/questions/58376589,以获取直接解决方案
jukzi,
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.