Recommand · October 14, 2021 0

ClassCastException on raw functional interface lambda invocation

Consider this example:

    Function<String, Integer> function = String::length;
    Function rawFunction = function; // warning: unchecked conversion
    rawFunction.apply(new Object()); // warning: unchecked call

The last line will give java.lang.ClassCastException: class java.lang. Object cannot be cast to class java.lang.String. This is not a surprise for me, as Function is declared to accept String.

I know that raw types can give ClassCastException, but all examples I saw are about ClassCastException on unchecked call return object not about method arguments: I can find cast added by a compiler in a bytecode for return object but not for arguments.
Where this behavior is specified? What exactly is causing ClassCastException from my example if not an instruction in a bytecode?

The way generics work with method parameters is via synthetic bridge methods.

For example, for the Function interface – whose raw parameter accepts an Object, but the non-raw parameter accepts a whatever, you have to have a method that accepts the Object.

When I decompiled the code above, I got a line in the bytecode:

16: invokeinterface #4,  2            // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;

so it’s actually trying to invoke a method apply, which takes an Object as the parameter. This method exists on the Function, this is the synthetic bridge method.

If you write a Function like this:

class MyFunction implements Function<String, Integer> {
  @Override public Integer apply(String input) {
    return null;
  }
}

and then decompile it, you will find there is another method there:

final class MyFunction implements java.util.function.Function<java.lang.String, java.lang.Integer> {
  // Default constructor, as you'd expect.
  MyFunction();

  // The apply method as defined above.
  public java.lang.Integer apply(java.lang.String);

  // What's this?!
  public java.lang.Object apply(java.lang.Object);
}

The public java.lang.Object apply(java.lang.Object) method has been added. This is the synthetic bridge method.

Its bytecode looks like:

  public java.lang.Object apply(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #2                  // class java/lang/String
       5: invokevirtual #3                  // Method apply:(Ljava/lang/String;)Ljava/lang/Integer;
       8: areturn

which is something like:

  public Object apply(Object input) {
    return apply((String) input);
  }

Hence, the synthetic bridge method just calls the "non-raw" apply method. So, the ClassCastException comes from that cast the synthetic bridge method.