jolice
jolice

Reputation: 98

Is it possible to get the type of lambda parameter at runtime?

I've encountered a type safety issue while applying Mockito's argument matchers.

Given the following interface:

interface SomeInterface {

    int method(Object x);

}

I'm trying to mock its only method and call it with the parameter that differs from the matcher type:

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

But the method invocation someInterface.method("X") produces the exception, namely:

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

However, when I expand a lambda to the anonymous class, everything works fine:

 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

As I can see from Mockito sources, the type of the matcher parameter is compared with the type of actual invocation argument. If the actual argument doesn't subclass the matcher's method parameter type (and thus can not be assigned to it), the matching is not performed:

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

However, it seems that this check doesn't pass for lambdas, apparently since it's not possible to retrieve the actual type of the lambda parameter at runtime (it's always just an Object type). Am I right about it?

I use mockito-core 3.0.3.

My Java configuration:

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)

Upvotes: 1

Views: 1325

Answers (2)

raner
raner

Reputation: 1295

@Holger's answer is 100% correct, but, struggling with a similar issue myself, I wanted to offer some additional thoughts.

This question demonstrates nicely that lambda expressions are completely different from anonymous classes, and that the former is not simply a shorter way of expressing the latter.

Let's say we define a method that determines the first implemented interface of a Function:

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

The following code will print the results for an anonymous class and for a lambda expression:

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));

Notably, the results are different:

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

For the anonymous class, the result is a ParameterizedType that preserves both type parameters of the function, whereas the result for the lambda expression is a raw type without type parameters.

Now, let's say we wanted to implement a type-safe dispatch (e.g., as an alternative to casting), similar to what Scala would allow us to do. In other words, given a number of functions, the dispatch method chooses the function whose parameter type matches the dispatch object's type and applies that function:

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";
    }
  });
}

In a very simplified version, such a dispatch method could be implemented as:

@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;
}

If we run

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

we get the expected

number
boolean

However, if we were to run

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

using lambda expressions instead of anonymous classes, the code will fail with a ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType, because the lambda expression implements a raw type.

This is one of the things that works with anonymous classes but not with lambda expressions.

Upvotes: 0

Holger
Holger

Reputation: 298143

My first reaction would be “just use intThat(i -> i == 42)”, but apparently, the implementation is dropping the information that it has an ArgumentMatcher<Integer> and later-on relying on the same Reflection approach that doesn’t work.

You can’t get the lambda parameter type reflectively and there are existing Q&As explaining why it’s not possible and not even intended. Note that this is not even lambda specific. There are scenarios with ordinary classes, where the type is not available too.

In the end, there is no point in trying to make this automatism work, when it requires additional declarations on the use side, like the explicit type cast (ArgumentMatcher<Integer>). E.g. instead of

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

you could also use

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

or even

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

Though, when we are at these trivial examples, eq(42) would do as well or even when(someInterface.method(42)).thenReturn(42).

However, when you often have to match complex integer expressions in such contexts with broader argument types, a solution close to your original attempt is to declare a reusable fixed type matcher interface

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

and use

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

i == 42 being a placeholder for a more complex expression

Upvotes: 3

Related Questions