是否可以在运行时获取lambda参数的类型?

时间:2019-11-22 10:06:14

标签: java generics lambda java-8 mockito

在应用Mockito的参数匹配器时,我遇到了类型安全问题。

给出以下界面:

interface SomeInterface {

    int method(Object x);

}

我正在尝试模拟其唯一方法,并使用与匹配器类型不同的参数进行调用:

SomeInterface someInterface = mock(SomeInterface.class);
when(someInterface.method(argThat((ArgumentMatcher<Integer>) integer -> integer == 42))).thenReturn(42);
someInterface.method("X"); // Throws ClassCastException

但是方法调用someInterface.method("X")会产生异常,即:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

但是,当我将lambda扩展到匿名类时,一切正常:

 SomeInterface someInterface = mock(SomeInterface.class);
 when(someInterface.method(argThat(new ArgumentMatcher<Integer>() {
     @Override
     public boolean matches(Integer integer) {
         return integer == 42;
     }
 }))).thenReturn(42);
 someInterface.method("X"); // OK, method invokes normally

正如我从Mockito来源看到的那样,将matcher参数的类型与实际调用参数的类型进行比较。如果实际参数不属于匹配器的方法参数类型的子类(因此无法分配给它),则不执行匹配:

private static boolean isCompatible(ArgumentMatcher<?> argumentMatcher, Object argument) {
        if (argument == null) {
            return true;
        } else {
            Class<?> expectedArgumentType = getArgumentType(argumentMatcher);
            return expectedArgumentType.isInstance(argument);
        }
    }

但是,对于lambda,似乎没有通过此检查,这显然是因为在运行时无法检索lambda参数的实际类型(它始终只是Object类型)。我对吗?

我使用 mockito-core 3.0.3

我的Java配置:

java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

2 个答案:

答案 0 :(得分:3)

我的第一个反应是“只使用intThat(i -> i == 42)”,但是显然,该实现正在删除具有ArgumentMatcher<Integer>的信息,并且以后依赖于没有工作。

您不能以反射的方式获取lambda参数类型,并且已有问题解答解释了为什么不可能甚至没有意图的原因。请注意,这甚至不是lambda特有的。在某些情况下,具有普通类的类型也不可用。

最后,当它需要在使用端进行其他声明时(例如显式类型强制转换(ArgumentMatcher<Integer>)),尝试使这种自动操作起作用没有任何意义。例如。代替

argThat((ArgumentMatcher<Integer>) integer -> integer == 42)

您也可以使用

argThat(obj -> obj instanceof Integer && (Integer)obj == 42)

甚至

argThat(Integer.valueOf(42)::equals)

尽管如此,当我们看这些琐碎的示例时,eq(42)甚至可以做到when(someInterface.method(42)).thenReturn(42)

但是,当您经常需要在这样的上下文中使用更广泛的参数类型来匹配复杂的整数表达式时,最接近您原始尝试的解决方案是声明一个可重用的固定类型匹配器接口

interface IntArgMatcher extends ArgumentMatcher<Integer> {
  @Override boolean matches(Integer arg0);
}

并使用

when(someInterface.method(argThat((IntArgMatcher)i -> i == 42))).thenReturn(42);

i == 42是更复杂的表达式的占位符

答案 1 :(得分:0)

@Holger的回答是100%正确的,但是,我自己也遇到类似的问题,我想提出一些其他想法。

这个问题很好地证明了lambda表达式与匿名类完全不同,并且前者不仅仅是表示后者的一种简短方法。

假设我们定义了一种方法,该方法确定Function的第一个实现的接口:

Type firstImplementedInterface(Function<?, ?> function)
{
  return function.getClass().getGenericInterfaces()[0];
}

以下代码将为匿名类和lambda表达式打印结果:

Function<Integer, Integer> plusOne = new Function<Integer, Integer>()
{
  @Override
  public Integer apply(Integer number)
  {
    return number+1;
  }
};
Function<Integer, Integer> plusOneLambda = number -> number+1;
System.err.println(firstImplementedInterface(plusOne));    
System.err.println(firstImplementedInterface(plusOneLambda));

值得注意的是,结果有所不同

java.util.function.Function<java.lang.Integer, java.lang.Integer>
interface java.util.function.Function

对于匿名类,结果是一个ParameterizedType,它保留了函数的两个类型参数,而lambda表达式的结果是没有类型参数的原始类型。

现在,我们要实现一种类型安全的分发(例如,作为强制转换的替代方法),类似于Scala允许我们执行的操作。换句话说,给定许多函数,调度方法选择参数类型与调度对象的类型匹配的函数,然后应用该函数:

String getTypeOf(Object object)
{
  return dispatch(object,
  new Function<Number, String>()
  {
    @Override
    public String apply(Number t)
    {
      return "number";
    }
  },
  new Function<Boolean, String>()
  {
    @Override
    public String apply(Boolean b)
    {
      return "boolean";
    }
  });
}

在非常简化的版本中,这样的调度方法可以实现为:

@SafeVarargs 
public final <T> T dispatch(Object dispatchObject, Function<?, T>... cases)
{
  @SuppressWarnings("unchecked")
  T result = Stream.of(cases)
    .filter(function -> parameterType(function).isAssignableFrom(dispatchObject.getClass()))
    .findFirst()
    .map(function -> ((Function<Object, T>)function).apply(dispatchObject))
    .orElseThrow(NoSuchElementException::new);
  return result;
}

Class<?> parameterType(Function<?, ?> function)
{
  ParameterizedType type = (ParameterizedType)function.getClass().getGenericInterfaces()[0];
  Type parameterType = type.getActualTypeArguments()[0];
  return (Class<?>)parameterType;
}

如果我们跑步

System.err.println(getTypeOf(42));
System.err.println(getTypeOf(true));

我们得到了预期的

number
boolean

但是,如果我们要跑步

System.err.println(dispatcher.<String>dispatch(Math.PI, (Number n) -> "number", (Boolean b) -> "boolean"));

使用lambda表达式代替匿名类,代码将失败并显示ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType,因为lambda表达式实现了原始类型。

这是适用于匿名类但不适用于lambda表达式的事物之一。