Reputation: 98
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
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
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