22

在一个包 ( a) 中,我有两个功能接口:

package a;

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

    void apply(A self);
}

-

package a;

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

超接口中的apply方法取self为 an A,否则,如果Applicable<A>改为使用,则该类型在包外将不可见,因此无法实现该方法。

在另一个包(b)中,我有以下Test类:

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);
    }
}

第一个实现使用匿名类,它可以正常工作。另一方面,第二个编译得很好,但在运行时失败,在尝试访问接口时抛出java.lang.BootstrapMethodError由 a 引起的 a 。java.lang.IllegalAccessErrorApplicable

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

我认为如果 lambda 表达式要么像匿名类一样工作,要么给出编译时错误,这将更有意义。所以,我只是想知道这里发生了什么。


我尝试删除超级接口并在其中声明方法,SomeApplicable如下所示:

package a;

@FunctionalInterface
public interface SomeApplicable {

    void apply(SomeApplicable self);
}

这显然使它工作,但让我们看到字节码有什么不同。

从 lambda 表达式编译的合成lambda$0方法在这两种情况下似乎相同,但我可以发现引导方法下方法参数的一个差异。

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

#59(La/Applicable;)V到的变化(La/SomeApplicable;)V

我真的不知道 lambda metafactory 是如何工作的,但我认为这可能是一个关键的区别。


我还尝试像这样显式声明该apply方法SomeApplicable

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {

    @Override
    void apply(SomeApplicable self);
}

现在该方法apply(SomeApplicable)实际存在并且编译器为apply(Applicable). 在运行时仍然会抛出相同的错误。

在字节码级别,它现在使用LambdaMetafactory.altMetafactory而不是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
4

1 回答 1

12

据我所知,JVM 做的一切都是正确的。

当在apply中声明方法Applicable但不在 中时SomeApplicable,匿名类应该可以工作,而 lambda 不应该。让我们检查一下字节码。

匿名课测试$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 生成接口方法的实现apply(Applicable)和被覆盖的方法apply(SomeApplicable)Applicable除了方法签名之外,这两个方法都没有引用不可访问的接口。也就是说,在匿名类的代码中的任何地方Applicable都没有解析接口(JVMS §5.4.3) 。

请注意,apply(Applicable)可以从 成功调用,因为方法签名中的类型在指令Test解析期间未解析(JVMS §5.4.3.4)invokeinterface

拉姆达

一个 lambda 实例是通过invokedynamic使用 bootstrap 方法执行字节码获得的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

用于构造 lambda 的静态参数是:

  1. 实现接口的 MethodType: void (a.Applicable);
  2. 将 MethodHandle 指向实现;
  3. lambda 表达式的有效 MethodType: void (a.SomeApplicable)

所有这些参数都在invokedynamic引导过程中解决(JVMS §5.4.3.6)

现在关键点:要解析 MethodType,其方法描述符中给出的所有类和接口都已解析(JVMS §5.4.3.5)。特别是,JVM 尝试a.Applicable代表Test类解析,并以IllegalAccessError. 然后,根据 的规范invokedynamic,将错误包装到BootstrapMethodError.

桥接法

要解决此问题IllegalAccessError,您需要在可公开访问的SomeApplicable接口中显式添加一个桥接方法:

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

在这种情况下,lambda 将实现apply(SomeApplicable)方法而不是apply(Applicable). 对应的invokedynamic指令会引用(La/SomeApplicable;)VMethodType,就会成功解析。

SomeApplicable注意:仅仅改变界面是不够的。您必须Test使用新版本重新编译SomeApplicable才能invokedynamic使用正确的 MethodTypes 生成。我已经在从 8u31 到最新的 9-ea 的几个 JDK 上验证了这一点,并且有问题的代码可以正常工作。

于 2016-10-30T23:15:27.757 回答