3

In my framework I have a class like this:

public class Foo<B, V> {
    private final Method getterMethod;
    ...

    public V executeGetter(B bean) {
        try {
           return getterMethod.invoke(bean);
        } catch ...
    }
}

This class is used to call getters of classes created by users that aren't available at compile time of my framework. For example, B might be a class called Person.

Through profiling, I've discovered that this method is horribly slow. The Method.invoke() takes 40% of performance in sampling profiling (even with setAccessible(true)), while a non reflective implementation takes only a small fraction of that performance.

So I 'd like to replace is with a MethodHandle:

public class Foo<B, V> {
    private final MethodHandle getterMethodHandle;
    ...

    public V executeGetter(B bean) {
        try {
           return getterMethodHandle.invoke(bean);
        } catch ...
    }
}

But then I get this exception:

java.lang.ClassCastException: Cannot cast [Ljava.lang.Object; to Person
    at sun.invoke.util.ValueConversions.newClassCastException(ValueConversions.java:461)
    at sun.invoke.util.ValueConversions.castReference(ValueConversions.java:456)
    at ...Foo.executeGetter(Foo.java:123)

even though bean is an instance of Person. Now the misleading part is that it's trying to cast an Object[] (and not an Object) to Person. Note that wrapping it in an object array (which is a performance loss) doesn't help:

 return getterMethodHandle.invoke(new Object[]{bean}); // Same exception

Is it possible to get the MethodHandle to work in this situation?

Geoffrey De Smet
  • 26,223
  • 11
  • 73
  • 120

2 Answers2

4

That ClassCastException only occurs if you compile with source/target level java 6.

Compile with source/target level 7 or higher to avoid that ClassCastException.

Answer found thanks to Tagir's answer. (vote up his answer too)

Geoffrey De Smet
  • 26,223
  • 11
  • 73
  • 120
3

Using MethodHandles in framework/library code is perfectly fine and I see no problem in your code. This example works just fine:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class Foo<B, V> {
    private final MethodHandle getterMethodHandle;

    public Foo(MethodHandle mh) {
        this.getterMethodHandle = mh;
    }

    public V executeGetter(B bean) {
        try {
           return (V) getterMethodHandle.invoke(bean);
        } catch(RuntimeException | Error ex) {
            throw ex;
        } catch(Throwable t) {
            throw new RuntimeException(t);
        }
    }

    static class Pojo {
        String x;

        public Pojo(String x) {
            this.x = x;
        }

        public String getX() {
            return x;
        }
    }

    public static void main(String[] args) throws Exception {
        // I prefer explicit use of findXYZ
        Foo<Pojo, String> foo = new Foo<>(MethodHandles.lookup()
                .findVirtual(Pojo.class, "getX", MethodType.methodType(String.class)));
        // Though unreflect also works fine
        Foo<Pojo, String> foo2 = new Foo<>(MethodHandles.lookup()
                .unreflect(Pojo.class.getMethod("getX")));

        System.out.println(foo.executeGetter(new Pojo("foo")));
        System.out.println(foo2.executeGetter(new Pojo("bar")));
    }
}

The output is:

foo
bar

For even better performance consider using invokeExact, though it will not allow you automatic type conversions like unboxing.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • Very interesting. You indeed don't compile time reference Pojo in the executeGetter method. I need to investigate why it doesn't work in my case. – Geoffrey De Smet Dec 02 '15 at 16:37
  • In JDK 1.7 with source and target level Java 6, this exact code *throws the same exception*: `java.lang.RuntimeException: java.lang.ClassCastException: Cannot cast [Ljava.lang.Object; to org.optaplanner.core.impl.domain.common.Foo$Pojo at ...Foo.executeGetter(Foo.java:34)` – Geoffrey De Smet Dec 03 '15 at 10:56
  • And **with the same JDK (1.7) but source and target level Java 7 it does work** (unlike with source/target java 6). – Geoffrey De Smet Dec 03 '15 at 11:19
  • 1
    @GeoffreyDeSmet, that's actually expected. There's special annotation `@PolymorphicSignature` on `invoke/invokeExact` methods which is specially handled by java compiler since Java 7, but such annotation means nothing when target is Java 6 or lower which results in incorrect bytecode generation. – Tagir Valeev Dec 03 '15 at 12:12
  • Makes sense now, although JDK 7 should have failed fast if it's compiling any code that uses MethodHandles with a target level of java 6. Thanks! PS: [I am baffled again for another reason with methodhandles](http://stackoverflow.com/questions/34069386/methodhandle-to-a-getter-setter-from-another-class-gvies-a-nosuchfielderror) – Geoffrey De Smet Dec 03 '15 at 15:11