Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda expression fails with a java.lang.BootstrapMethodError at runtime

In one package (a) I have two functional interfaces:

package a;

@FunctionalInterface
interface Applicable<A extends Applicable<A>> {

    void apply(A self);
}

-

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
}

The apply method in the superinterface takes self as an A because otherwise, if Applicable<A> was used instead, the type would not be visible outside the package and therefore the method couldn't be implemented.

In another package (b), I have the following Test class:

package b;

import a.SomeApplicable;

public class Test {

    public static void main(String[] args) {

        // implement using an anonymous class
        SomeApplicable a = new SomeApplicable() {
            @Override
            public void apply(SomeApplicable self) {
                System.out.println("a");
            }
        };
        a.apply(a);

        // implement using a lambda expression
        SomeApplicable b = (SomeApplicable self) -> System.out.println("b");
        b.apply(b);
    }
}

The first implementation uses an anonymous class and it works with no problem. The second one, on the other hand, compiles fine but fails at runtime throwing a java.lang.BootstrapMethodError caused by a java.lang.IllegalAccessError as it tries to access the Applicable interface.

Exception in thread "main" java.lang.BootstrapMethodError: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
    at b.Test.main(Test.java:19)
Caused by: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
    ... 1 more

I think it would make more sense if the lambda expression either worked just like the anonymous class or gave a compile-time error. So, I'm just wondering what is going on here.


I tried removing the superinterface and declaring the method within SomeApplicable like this:

package a;

@FunctionalInterface
public interface SomeApplicable {

    void apply(SomeApplicable self);
}

This obviously makes it work but allows us to see what's different in bytecode.

The synthetic lambda$0 method compiled from the lambda expression seems identical in both cases, but I could spot one difference in the method arguments under bootstrap methods.

Bootstrap methods:
  0 : # 58 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
        #59 (La/Applicable;)V
        #62 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
        #63 (La/SomeApplicable;)V

The #59 changes from (La/Applicable;)V to (La/SomeApplicable;)V.

I don't really know how lambda metafactory works but I think this might be a key difference.


I also tried explicitly declaring the apply method in SomeApplicable like this:

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {

    @Override
    void apply(SomeApplicable self);
}

Now the method apply(SomeApplicable) actually exists and the compiler generates a bridge method for apply(Applicable). Still the same error is thrown at runtime.

At bytecode level it now uses LambdaMetafactory.altMetafactory instead of LambdaMetafactory.metafactory:

Bootstrap methods:
  0 : # 57 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
        #58 (La/SomeApplicable;)V
        #61 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
        #62 (La/SomeApplicable;)V
        #63 4
        #64 1
        #66 (La/Applicable;)V
like image 204
Bubletan Avatar asked Oct 26 '16 19:10

Bubletan


1 Answers

As far as I see, JVM does everything right.

When apply method is declared in Applicable, but not in SomeApplicable, the anonymous class should work, and the lambda should not. Let's examine the bytecode.

Anonymous class Test$1

public void apply(a.SomeApplicable);
  Code:
     0: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #3    // String a
     5: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return

public void apply(a.Applicable);
  Code:
     0: aload_0
     1: aload_1
     2: checkcast     #5    // class a/SomeApplicable
     5: invokevirtual #6    // Method apply:(La/SomeApplicable;)V
     8: return

javac generates both the implementation of interface method apply(Applicable) and the overriden method apply(SomeApplicable). Neither of methods refer to inaccessible interface Applicable, except in the method signature. That is, Applicable interface is not resolved (JVMS §5.4.3) anywhere in the code of anonymous class.

Note that apply(Applicable) can be successfully called from Test, because types in the method signature are not resolved during the resolution of invokeinterface instruction (JVMS §5.4.3.4).

Lambda

An instance of lambda is obtained by execution of invokedynamic bytecode with the bootstrap method LambdaMetafactory.metafactory:

BootstrapMethods:
  0: #36 invokestatic java/lang/invoke/LambdaMetafactory.metafactory
    Method arguments:
      #37 (La/Applicable;)V
      #38 invokestatic b/Test.lambda$main$0:(La/SomeApplicable;)V
      #39 (La/SomeApplicable;)V

The static arguments used to construct lambda are:

  1. MethodType of the implemented interface: void (a.Applicable);
  2. Direct MethodHandle to the implementation;
  3. Effective MethodType of the lambda expression: void (a.SomeApplicable).

All these arguments are resolved during invokedynamic bootstrap process (JVMS §5.4.3.6).

Now the key point: to resolve a MethodType all classes and interfaces given in its method descriptor are resolved (JVMS §5.4.3.5). In particular, JVM tries to resolve a.Applicable on behalf of Test class, and fails with IllegalAccessError. Then, according to the spec of invokedynamic, the error is wrapped into BootstrapMethodError.

Bridge method

To work around IllegalAccessError, you need to explicitly add a bridge method in publicly accessible SomeApplicable interface:

public interface SomeApplicable extends Applicable<SomeApplicable> {
    @Override
    void apply(SomeApplicable self);
}

In this case lambda will implement apply(SomeApplicable) method instead of apply(Applicable). The corresponding invokedynamic instruction will refer to (La/SomeApplicable;)V MethodType, which will be successfully resolved.

Note: it is not enough to change just SomeApplicable interface. You'll have to recompile Test with the new version of SomeApplicable in order to generate invokedynamic with the proper MethodTypes. I've verified this on several JDKs from 8u31 to the latest 9-ea, and the code in question worked without errors.

like image 147
apangin Avatar answered Oct 31 '22 01:10

apangin