javaarraysmethodhandle

"Cannot cast [Ljava.lang.Object;" passing Object[] to Object... varargs


I'm struggling with a simple, fundamental Java language question that isn't making sense. I'm passing an Object[] to a method which expects varargs Object..., but the method seems to be interpreting the request as new Object[]{Object[]}. That is, instead of considering the Object[] as equivalent to Object..., it seems to be passing the Object[] as the first element of the varargs, thus wrapping the Object[] in another array of objects.

I have the following in Java 17:

FooImpl foo = goGetFooImpl();
List<?> methodArgs = List.of(); //no method args
Object[] invokeArgs = Stream.concat(Stream.of(foo, methodArgs.stream()).toArray();
// this gives me the equivalent of `new Object[]{foo}`
// logging `List.of(invokeArgs)` shows `[com.example.FooImpl@…]`
MethodHandle barMethodHandle = goLookUpMethodHandle(foo, "bar");
methodHandle.invoke(invokeArgs); //expects varargs `foo, arg1, arg2, …`

I get:

java.lang.ClassCastException: Cannot cast [Ljava.lang.Object; to com.example.FooImpl

The method signature I'm calling is MethodHandle.invoke(Object... args). It expects varargs, with the first object referencing the target object on which the method is being invoked. So this works:

FooImpl foo = goGetFooImpl();
MethodHandle barMethodHandle = goLookUpMethodHandle(foo, "bar");
barMethodHandle.invoke(foo);

In this case I don't know ahead of time that there are no further arguments (i.e. methodArgs above might not be empty), which is why I combined everything into an Object[]. But even manually testing like this doesn't work:

FooImpl foo = goGetFooImpl();
MethodHandle barMethodHandle = goLookUpMethodHandle(foo, "bar");
barMethodHandle.invoke(new Object[]{foo});

The reason this is confusing me is that this is such a basic Java issue (a newbie question, really), and I have understood for years that Java would by default consider passing Object[] to a method expecting Object... to be equivalent. I had believed that I would have to cast Object[] to Object using (Object)invokeArgs in order for it to be considered the first object of the Object... (which I don't want). But this seems to be happening anyway without the cast.

The error message seems to be telling me that Java is considering Object[] to be the first Object of the Object... varargs, because the method seems to be trying to convert the invokeArgs itself (which is a Object[]) to a reference to FooImpl, which is what is requested as the first argument in the varargs.

This doesn't make sense to me. Either I've misunderstood for years how varargs works, or something weird is going on with the MethodHandle.invoke(…) method specifically, or I'm just having a mental blank.

Even if MethodHandle.invoke(…) has some sort of special behavior, aren't the arguments determined at compile time? How could MethodHandle.invoke(…) have different behavior based upon whether I use invoke(foo) or invoke(new Object[]{foo})? During compilation aren't they equivalent, and in either case the compiler will wind up passing new Object[]{foo} to the method, even in the case of invoke(foo)?


Solution

  • or something weird is going on with the MethodHandle.invoke(…) method specifically

    Yes! The signature of MethodHandle::invoke is a lie. It is not a Object... varargs method, and it does not return Object either. Both the formal parameters and the return type are placeholders. This is because MethodHandle::invoke is a signature polymorphic method.

    See section 15.12.3 of the Java language spec (se20). Quoted here, I've omitted the irrelevant parts:

    A method is signature polymorphic if all of the following are true:

    • It is declared in the java.lang.invoke.MethodHandle class or the java.lang.invoke.VarHandle class.

    • It has a single variable arity parameter (§8.4.1) whose declared type is Object[].

    • It is native.

    This affects how the he compile-time parameter types and compile-time result are determined:

    • If the compile-time declaration for the method invocation is a signature polymorphic method, then:

      • The compile-time parameter types are the types of the actual argument expressions. ...

      • The compile-time result is determined as follows:

        ...

        • Otherwise, if the method invocation expression is an expression statement, the compile-time result is void.

    (See also the section of the MethodHandle javadoc on signature polymorphism)

    In your case the type of the single actual argument expression is Object[], so the type of the invocation is (Object[])void.

    The invocation then fails because the implementation of invoke tries to automatically adapt the argument type to the parameter type of the method handle, which is com.example.FooImpl. For reference types this is done using a cast, as explained in the documentation of MethodHandle::asType. Since Object[] can not be cast to FooImpl, you get the ClassCastException.

    As @Louis Wasserman says in the comments: if you want the Object[] to be expanded into a list of arguments you need to use MethodHandle::invokeWithArguments.