3

I have a few classes that each implement an interface. From these classes I search out a method using an annotation. This method returns a boolean and always has an object as parameter, which always inherits from another fixed object. Now I want to create a functional interface from this method. Optimally of course a Predicate, which takes over the mentioned parameter. This I have now tried for some time to implement with LambdaMetafactory:

private Predicate<Parent> toPredicate(final ExampleInterface instance, final Method method) {
    try {
        final MethodHandle handle = LOOKUP.unreflect(method);
        final MethodType signature = MethodType.methodType(boolean.class, method.getParameters()[0].getType());

        final CallSite callSite = LambdaMetafactory.metafactory(
            LOOKUP,
            "test",
            MethodType.methodType(Predicate.class, instance.getClass()),
            signature,
            handle,
            signature
        );

        return (Predicate<Parent>) callSite.getTarget().invoke(instance);
    } catch (Throwable e) {
        e.printStackTrace();
        return null;
    }
}

My problem now is that when I call the Predicate's test method, an AbstractMethodError is thrown. When I use signature with (boolean.class, Parent.class) I get a LambdaConversionException. Is it even possible to implement it that dynamically? If yes, how?

zuzuri
  • 45
  • 6
  • 2
    The fourth parameter of `metafactory` should be `signature.erase()`. That said, do you realise that a `Predicate` is not a `Predicate`? The latter can test a dog, but the former cannot. It's the other way around - a `Predicate` is a `Predicate super Cat>`. – Sweeper Feb 15 '23 at 00:43
  • I had tried it before with 'Object.class' and not 'erase()' to perform type erasure. So does that make a difference? Anyway, it finally works now, thank you very much! And yes, I mean to understand the difference between 'Predicate' and 'Predicate' - that's simple inheritance after all. Or do you still see a mistake somewhere in my approach? – zuzuri Feb 15 '23 at 00:51
  • 1
    My point is that "This method returns a boolean and always has an object as parameter, which always inherits from another fixed object." is not a `Predicate`. It is not type safe to cast it to that. – Sweeper Feb 15 '23 at 00:54
  • I think I understand. But how would you get around that and what other problems might arise otherwise? – zuzuri Feb 15 '23 at 01:07

1 Answers1

4

Since you know the target interface type is Predicate you could use a lambda expression as well:

private Predicate<Parent> toPredicate(final ExampleInterface instance, final Method method) {
    final MethodHandle handle = LOOKUP.unreflect(method)
        .bindTo(instance)
        .asType(MethodType.methodType(boolean.class, Parent.class);
    
    return parent -> {
        try {
            return (boolean) handle.invokeExact(parent);
        } catch (Throwable t) {
            throw new RuntimeException("Should not happen", t);
        }
    };
}

Using LambdaMetafactory is only really beneficial in specific cases where a particular call site only sees 1 or 2 implementations of the interface. Also note that every time you create a metafactory, a new class is generated (unless it can be loaded from the CDS archive), which has its own costs associated with it.

Also, depending on the Java version you are using, these classes are strongly tied to the defining class loader. This started when the implementation was switched to hidden classes in Java 15. So, e.g. if the defining class loader is the application class loader, the classes are in practice never unloaded, and this can lead to metaspace exhaustion if metafactory is called a lot. (for regular use through invokedynamic the resolved instruction keeps the class alive any way, and strongly tying the class to the class loader saves memory elsewhere).

The thing you're getting wrong is the interface method type. It should be the erasure of the test method in Predicate:

final MethodHandle handle = LOOKUP.unreflect(method);

final CallSite callSite = LambdaMetafactory.metafactory(
    LOOKUP,
    "test",
    MethodType.methodType(Predicate.class, instance.getClass()),
    MethodType.methodType(boolean.class, Object.class),
    handle,
    MethodType.methodType(boolean.class, Parent.class)
);
Jorn Vernee
  • 31,735
  • 4
  • 76
  • 93
  • The reason why I chose my current solution is that my program needs to be very performant. It seems to me that your first solution creates a Predicate that calls a MethodHandle every time. MethodHandles are unfortunately much slower in the benchmark than the functional interfaces generated by LambdaMetafactory. Or am I wrong here? Nevertheless, thanks for this elegant approach, which also worked. – zuzuri Feb 15 '23 at 01:01
  • @zuzuri I can't really say without seeing the exact benchmark you're using. After JIT compilation, the only overhead you get from the method handle call is a jump through a small assembly stub which from my own tests is not much slower than a normal Java method call. The main benefit of LMF comes from inlining through the interface method (which might never happen in practice). Also, as I said, the LMF generates a new class every time you create one, so you might be shooting yourself in the foot with that in terms of performance. – Jorn Vernee Feb 15 '23 at 01:18
  • Well, I create the predicates only at the start of the program, but not in the subsequent runtime. Therefore this should be ok, if I have understood this correctly. I will do another benchmark later and see if anything surprises me. – zuzuri Feb 15 '23 at 01:21
  • I have now designed a small benchmark in my program, not a raw one, so it should not be trusted 100%. Anyway, I ended up with a runtime difference of 10%, where the method with LMF seems to be a bit faster. However, anyone else reading this should design their own benchmark again and not rely on my value. – zuzuri Feb 15 '23 at 01:49
  • 1
    Classes created by `LambdaMetafactory` are *not* strongly tied to the loader. They are [hidden classes](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineHiddenClass(byte%5B%5D,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...)) (while the public API is new, [the feature](https://web.archive.org/web/20160506224222/https://blogs.oracle.com/jrose/entry/anonymous_classes_in_the_vm) exists since Java 7) and may get garbage collected which you can test with the code of [this answer](https://stackoverflow.com/a/34931968/2711488). – Holger Feb 28 '23 at 08:20
  • @Holger The classes produced by `LambdaMetafactory` _are_ strongly tied to the class loader. They are defined with the [`STRONG`](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.ClassOption.html#STRONG) option in `InnerClassLambdaMetafactory`: https://github.com/openjdk/jdk/blob/5feb13b55d32fad8f533f52ee7bd63e2cf2d247c/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java#L370 (and have been since they use hidden classes). We've been recently discussing this as well: https://github.com/openjdk/jdk/pull/12493 – Jorn Vernee Feb 28 '23 at 12:42
  • @Holger it looks like you tested this on Java 8, where indeed the classes did not have a strong tie to the class loader. If I run the test on java 19, I never see "class collected" printed. The strong tie was added when switching to hidden classes to avoid having a `ClassLoaderData` per generated class (i.e. it saves memory). As you say, the resolved `invokedynamic` keeps the class alive any way. – Jorn Vernee Feb 28 '23 at 12:54
  • It's true that this isn't the case on all versions though. I've amended to answer to reflect that. – Jorn Vernee Feb 28 '23 at 13:02
  • 1
    Oh, I always wondered why anyone would ever use the `STRONG` option. So, instead of fixing the problem with non-strong hidden classes, `LambdaMetaFactory` uses those strongly linked classes and everything is back to square one, as `MethodHandle` uses hidden classes behind the scenes (I really hope, those are still not strongly linked)… – Holger Mar 01 '23 at 07:21