Lambda expression fails with a java.lang.BootstrapMethodError at runtime
Asked Answered
W

1

22

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
Warty answered 26/10, 2016 at 19:11 Comment(9)
Can you please provide the full stacktrace? Throwing Error sounds very suspicous.Keelboat
@Keelboat There isn't much to see in the stacktrace: Caused by: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test at the line of the lambda expression.Warty
Given your description I am not sure if that "DUP" close is legit. If I were you, I would create a complete minimal viable example and put that into your question. If you can show that one piece of code, compiled out of one file leads to this error, then that DUP doesn't match; and you should ask to reopen.Keelboat
@Keelboat I don't think it's possible to get this error without two packages, the superinterface must not be visible.Warty
I can't even compile this. I get the complaint The type Applicable<SomeApplicable> from the descriptor computed for the target context is not visible here.Rheo
Still. If you can show that one only has to copy your input into two classes, and the whole thing blows, then you are right. Otherwise, you are really dealing with some inconsistent class files.Keelboat
And I would argue that multi-file MCVEs are acceptable - the important part is the minimal part... minimal doesn't have to mean single-file, but it does mean "don't fill my browser cache".Rheo
I edited the example so that it can be copy-pasted. It's three files, two in package a and one in package b.Warty
I'll reopen. I can reproduce with javac, not with Eclipse, maybe bug.Cockscomb
C
12

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.

Cermet answered 30/10, 2016 at 23:15 Comment(2)
The bridge method workaround doesn't seem to work if compiled with Eclipse. Now that I tried it with javac it worked as expected. For some reason the Eclipse compiler uses altMetafactory with BRIDGES flag (and (La/Applicable;)V as the bridge method type), which causes the same error to occur. Another simple workaround that I figured out would be declaring Applicable as Applicable<A extends Object & Applicable<A>>. Then the parameter type would be public as it gets erased to Object.Warty
Anyway, if JVM does everything correctly, it looks like a compiler problem to me. Giving an error or using some possible workaround would be a better option than just accepting illegal code silently.Warty

© 2022 - 2024 — McMap. All rights reserved.