Lambda表达式在运行时

时间:2016-10-26 19:11:53

标签: java generics lambda package-private

在一个包(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作为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尝试访问java.lang.IllegalAccessError接口而导致Applicable失败。

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方法下方法参数的一个区别。

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

1 个答案:

答案 0 :(得分:11)

据我所知,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,因为在解析invokeinterface指令(JVMS §5.4.3.4)期间,方法签名中的类型无法解析。

LAMBDA

通过使用引导方法invokedynamic执行LambdaMetafactory.metafactory字节码来获取lambda的实例:

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)
  4. 所有这些参数都在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;)V MethodType,它将成功解析。

    注意:仅更改SomeApplicable接口是不够的。您必须使用新版本的Test重新编译SomeApplicable,才能使用正确的MethodTypes生成invokedynamic。我已经在从8u31到最新的9-ea的几个JDK上验证了这一点,并且所讨论的代码没有错误地工作。