Lambda 表达式介绍和底层实现分析

如果你的需求需要匿名类来实现,例如是一个只有一个方法的接口,那么匿名类的语法可能看起来比较笨拙和不清晰,尽管匿名类比命名类更简洁,但对于只有一个方法的类来说,即使是匿名类也显得有些麻烦。还有在一些情况下,需要将功能作为参数传递给另一个方法,例如当有人单击页面上按钮时应该采取什么操作,javascript 可以通过闭包实现。在 java 语言中,lambda 表达式能够将功能视为方法参数,或将代码视为数据,而且 lambda 表达式可以更紧凑地表达单方法类的实例,在 Swing 编程和集合(Collections)编程中优势很明显。

lambda 表达式

lambda 表达式,也被称为闭包,它是推动 Java 8 发布的最重要新特性。lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

lambda 表达式的语法格式如下:

(parameters) -> expression或 (parameters) ->{ statements; }

以下是 lambda 表达式的重要特征:

file

例如下面是一些 lambda 表达式

(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

函数式接口

只有一个抽象方法的接口,称为函数式接口。例如下面的接口,

public interface MyFunctionInterface<T> {
    public T getValue(T t);
}

测试类如下

public class MyFunctionInterfaceTest {
    public static void main(String[] args) {
        testMethod("   aaaa  ", s -> s.trim());
        testMethod("   aaaa  ", s -> s.trim().toUpperCase());
    }

    public static void testMethod(String str,     MyFunctionInterface<String> functionInterface) {
        System.out.println(functionInterface.getValue(str));
    }
}

输出结果如下:

aaaa
AAAA

修改一下上面的接口,增加一个方法。

public interface MyFunctionInterface<T> {
    public T getValue(T t);
    public T returnValue(T t);
}

增加一个方法之后,上面就不是函数式接口了,可以看到 lambda 表达式就会报错。

file

虽然不能在函数式接口中定义多个方法,但可以定义默认方法、静态方法以及 java.lang.Object 里的 public 方法。如下面的代码

@FunctionalInterface
public interface MyFunctionInterface<T> {

    public T getValue(T t);

    default void defaultMethod() {
        System.out.println("this is default method");
    }

    static void staticMethod() {
        System.out.println("this is static method");
    }


    public boolean equals(Object obj);
}

我们可以在接口上使用 @FunctionalInterface 注解,如果使用 Intellij IDEA 可以在编码的时候就看见报错了,这样做可以检查它是否是一个函数式接口。

file

通过反编译,可以看到函数式接口其实就是一个普通的 java 接口类,如下图

file

函数式接口可以作为方法参数传递 lambda 表达式,但是为了将 Lambda 表达式作为参数传递,接收 Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。但是我们没必要为每一个 lambda 表达式创建接口,在 jdk 的 java.util.function 包下面已经为我们创建了常用的函数式接口,其中比较核心的是消费型接口(Consumer ),供给型接口(Supplier ),断言型接口(Predicate ),函数型接口(Function<T, R>)四个接口,能够满足大部分应用场景。

lambda 表达式原理分析

继续使用上面的测试代码,可以在 IDEA 中使用 jclasslib Bytecode viewer 插件查看 MyFunctionInterface.class 源文件。

安装完 jclasslib Bytecode viewer,会在 view 菜单中出现如下两个选项

file

选择需要反编译的 class 文件,点击 Show Bytecode with Jclasslib 选项会出现如下界面

file

可以看到里面编译器多生成了 lambda$main$0 和 lambda$main$1 两个私有静态方法,属性当中多了 InnerClasses。我们可以通过 Show Bytecode 查看一下测试类字节码更详细的反编译结果,找到这两个静态方法。

// access flags 0x100A
private static synthetic lambda$main$1(Ljava/lang/String;)Ljava/lang/String;
 L0
  LINENUMBER 6 L0
  ALOAD 0
  INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String;
  INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;
  ARETURN
 L1
  LOCALVARIABLE s Ljava/lang/String; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1

// access flags 0x100A
private static synthetic lambda$main$0(Ljava/lang/String;)Ljava/lang/String;
 L0
  LINENUMBER 5 L0
  ALOAD 0
  INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String;
  ARETURN
 L1
  LOCALVARIABLE s Ljava/lang/String; L0 L1 0
  MAXSTACK = 1
  MAXLOCALS = 1
}

可以看到两个私有的静态方法干的就是 Lambda 表达式里面的内容,那么又是如何调用的生成的私有静态方法呢?如下图,通过分析 main 方法的 L0,首先通过 INVOKEDYNAMIC 指令调用是 MyFunctionInterface 的 getValue 方法的引用,以及后面的 BootstrapMethods #0。使用 jclasslib Bytecode viewer 查看。

file

点击 #3,进入下面界面

file

点击 BootstrapMethods #0,进入如下界面

file

继续点击相应的方法描述符,我们可以看到最后

file

cp_info #74 内容如下:

(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;

可以看到 INVOKEDYNAMIC 后面的一系列指令,最后使用 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;

查看 LambdaMetafactory.metafactory 的方法,里面通过 InnerClassLambdaMetafactory 生成了 CallSite 的子类 ConstantCallSite,当通过指令调用 CallSite 会返回函数式接口的实例,而生成接口实例的方式是通过内部类的方式,由于方法比较深,就不继续贴代码了。

public static CallSite metafactory(MethodHandles.Lookup caller,
                                   String invokedName,
                                   MethodType invokedType,
                                   MethodType samMethodType,
                                   MethodHandle implMethod,
                                   MethodType instantiatedMethodType)
        throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf;
    mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                         invokedName, samMethodType,
                                         implMethod, instantiatedMethodType,
                                         false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

file

CallSite 持有 com/para/lambda/MyFunctionInterfaceTest.lambda$main$0 方法的句柄,这个句柄会调用该方法。

file

所以使用 lambda 表达式的地方,会在类编译的时候在本类中生成对应的私有静态方法和一个 INNERCLASS 的访问标识(具体是什么东西没找到资料,注释显示是一个访问标识),该访问标识会调用引导类加载器动态生成内部类,该内部类实现了函数式接口,在实现接口的方法中,会调用编译器生成静态方法,在使用 lambda 表达式的地方,通过传递内部类实例,来调用函数式接口方法。

总结

本文从 lambda 表达式、函数式接口的介绍和对 lambda 表达式底层原理的分析来认识 java 中的函数式编程。函数式接口本身就是一个普通的接口,而 lambda 表达式本质上和匿名内部类是一样的,只不过条件更加苛刻。使用 lamda 表达式可以以一种更优雅的方式来编程。

本文转载自:360 技术(ID:qihoo_tech) 点这里:2020Python高薪实战学习大合集**

[拿走不谢!Python 3.9 官方中文文档,限时领!] (http://dwz.date/dE6v)

[限时!速领!14张高清Python速查表,效率提升必备!] (http://dwz.date/dE6w)

[GitHub标星3W+,80个Python案例,带你轻松玩转Python学习!] (http://dwz.date/dE64)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页