阅读 689

理解和运用Java中的Lambda

前提

回想一下,JDK8是2014年发布正式版的,到现在为(2020-02-08)止已经过去了5年多。JDK8引入的两个比较强大的新特性是Lambda表达式(下文的Lambda特指JDK提供的Lambda)和Stream,这两个强大的特性让函数式编程在Java开发中发扬光大。这篇文章会从基本概念、使用方式、实现原理和实战场景等角度介绍Lambda的全貌,其中还会涉及一些函数式编程概念、JVM一些知识等等。

基本概念

下面介绍一些基本概念,一步一步引出Lambda的概念。

函数式接口

函数式接口和接口默认方法都是JDK8引入的新特性。函数式接口的概念可以从java.lang.FunctionalInterface注解的API注释中得知:

An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification.

Conceptually, a functional interface has exactly one abstract method. Since {@linkplain java.lang.reflect.Method#isDefault() default methods} have an implementation, they are not abstract.

简单来说就是:@FunctionalInterface是一个提供信息的接口(其实就是标识接口),用于表明对应的接口类型声明是一个Java语言规范定义的函数式接口。从概念上说,一个函数式接口有且仅有一个抽象方法,因为接口默认方法必须予以实现,它们不是抽象方法。

所以可以这样给函数式接口定义:如果一个接口声明的时候有且仅有一个抽象方法,那么它就是函数式接口,可以使用@FunctionalInterface注解标识。

JDK中已经定义了很多内置的函数式接口,例如:

// java.lang.Runnable
@FunctionalInterface
public interface Runnable {

    public abstract void run();
}  

// java.util.function.Supplier
@FunctionalInterface
public interface Supplier<T> {

    T get();
}
复制代码

也可以自定义函数式接口,例如:

@FunctionalInterface
public interface CustomFunctionalInterface {
    
    // 可以缩写为void process();  接口方法定义的时候,默认使用public abstract修饰
    public abstract void process();
}
复制代码

接口默认方法

接口默认方法的含义可以见Java官方教程中对应的章节,在文末的参考资料可以查看具体的链接:

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

简单来说就是:默认方法允许你在你的类库中向接口添加新的功能,并确保新增的默认方法与这些接口的较早版本编写的代码二进制兼容

接口默认方法(下称默认方法)通过default关键字声明,可以直接在接口中编写方法体。也就是默认方法既声明了方法,也实现了方法。这一点很重要,在默认方法特性出现之前,Java编程语言规范中,接口的本质就是方法声明的集合体,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:

public interface DefaultMethod {

    default void defaultVoidMethod() {

    }

    default String sayHello(String name) {
        return String.format("%s say hello!", name);
    }

    static void main(String[] args) throws Exception {
        class Impl implements DefaultMethod {

        }
        DefaultMethod defaultMethod = new Impl();
        System.out.println(defaultMethod.sayHello("throwable"));  // throwable say hello!
    }
}
复制代码

如果继承一个定义了默认方法的接口,那么可以有如下的做法:

  • 完全忽略父接口的默认方法,那么相当于直接继承父接口的默认方法的实现(方法继承)。
  • 重新声明默认方法,这里特指去掉default关键字,用public abstract关键字重新声明对应的方法,相当于让默认方法转变为抽象方法,子类需要进行实现(方法抽象)。
  • 重新定义默认方法,也就是直接覆盖父接口中的实现(方法覆盖)。

结合前面一节提到的函数式接口,这里可以综合得出一个结论:函数式接口,也就是有且仅有一个抽象方法的接口,可以定义0个或者N(N >= 1)个默认方法。这一点正是Stream特性引入的理论基础。举个例子:

@FunctionalInterface
public interface CustomFunctionalInterface {

    public abstract void process();

    default void defaultVoidMethod() {

    }

    default String sayHello(String name) {
        return String.format("%s say hello!", name);
    }
}
复制代码

这里说点题外话。

在写这篇文章的时候,笔者想起了一个前同事说过的话,大意如下:在软件工程中,如果从零做起,任何新功能的开发都是十分简单的,困难的是在兼容所有历史功能的前提下进行新功能的迭代。试想一下,Java迭代到今天已经过去十多年了,Hotspot VM源码工程已经十分庞大(手动编译过OpenJDK Hotspot VM源码的人都知道过程的痛苦),任何新增的特性都要向前兼容,否则很多用了历史版本的Java应用会无法升级新的JDK版本。既要二进制向前兼容,又要迭代出新的特性,Java需要进行舍夺,默认方法就是一个例子,必须舍去接口只能定义抽象方法这个延续了多年在Java开发者中根深蒂固的概念,夺取了基于默认方法实现构筑出来的流式编程体系。笔者有时候也在思考:如果要我去开发Stream这个新特性,我会怎么做或者我能怎么做?

嵌套类(Nested Classes)

嵌套类(Nested Classes),简单来说就是:在一个类中定义另一个类,那么在类内被定义的那个类就是嵌套类,最外层的类一般称为封闭类(Enclosing Class)。嵌套类主要分为两种:静态嵌套类和非静态嵌套类,而非静态嵌套类又称为内部类(Inner Classes

// 封闭类
class OuterClass {
    ...
    // 静态嵌套类
    static class StaticNestedClass {
        ...
    }
    
    // 内部类
    class InnerClass {
        ...
    }
}
复制代码

静态嵌套类可以直接使用封闭的类名称去访问例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();,这种使用形式和一般类实例化基本没有区别。

内部类实例的存在必须依赖于封闭类实例的存在,并且内部类可以直接访问封闭类的任意属性和方法,简单来说就是内部类的实例化必须在封闭类实例化之后,并且依赖于封闭类的实例,声明的语法有点奇特:

public class OuterClass {

    int x = 1;

    static class StaticNestedClass {

    }

    class InnerClass {
        // 内部类可以访问封闭类的属性
        int y = x;
    }

    public static void main(String[] args) throws Exception {
        OuterClass outerClass = new OuterClass();

        // 必须这样实例化内部类 - 声明的语法相对奇特
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();

        // 静态嵌套类可以一般实例化,形式为:封闭类.静态嵌套类
        OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass();

        // 如果main方法在封闭类内,可以直接使用静态嵌套类进行实例化
        StaticNestedClass x = new StaticNestedClass();
    }
}
复制代码

内部类中有两种特殊的类型:本地类(Local Classes)和匿名类(Anonymous Classes)。

本地类是一种声明在任意块(block)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:

public class OuterClass {

    static int y = 1;
    
    {    
        // 本地类A
        class A{
            int z = y;
        }
        A a = new A();
    }

    static {
        // 本地类B
        class B{
            int z = y;
        }
        B b = new B();
    }

    private void method(){
        // 本地类C
        class C{
            int z = y;
        }
        C c = new C();
    }
}
复制代码

匿名类可以让代码更加简明,允许使用者在定义类的同时予以实现,匿名类和其他内部类不同的地方是:它是一种表达式,而不是类声明。例如:

public class OuterClass {

    interface In {

        void method(String value);
    }
    
    public void sayHello(){
        // 本地类 - 类声明
        class LocalClass{
            
        }
        
        // 匿名类 - 是一个表达式
        In in = new In() {
            
            @Override
            public void method(String value) {
                
            }
        };
    }
}
复制代码

如果用Java做过GUI开发,匿名类在Swing或者JavaFx的事件回调中大量使用,经常会看到类似这样的代码:

JButton button = new JButton();
button.addActionListener(new AbstractAction() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按钮事件被触发...");
    }
});
复制代码

嵌套类的类型关系图如下:

Nested Classes
  - Static Nested Classes
  - None Nested Classes
    - Local Classes
    - Anonymous Classes
    - Other Inner Classes
复制代码

Lambda表达式

下面是来自某搜索引擎百科关于Lambda表达式的定义:

Lambda表达式(Lambda Expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的Lambda抽象(Lambda Abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

Java中的Lambda表达式(下面称Lambda)表面上和上面的定义类似,本质也是匿名函数,但其实现原理区别于一般的匿名类中的匿名函数实现,她是JDK8引入的一颗新的语法糖。

引入Lambda表达式的初衷

如果一个接口只包含一个方法,那么匿名类的语法会变得十分笨拙和不清楚,产生大量的模板代码,归结一下就是:代码冗余是匿名类的最大弊端。在编程的时候,我们很多时候希望把功能作为参数传递到另一个方法,Lambda就是为此而生,Lambda允许使用者将功能视为方法参数,将代码视为数据。引入Lambda带来了如下优势:

  • 简化代码,引入了强大的类型推断和方法引用特性,简单的功能甚至可以一行代码解决,解放匿名类的束缚。
  • 把功能作为参数向下传递,为函数式编程提供了支持。

至此还得出一个结论:Lambda只适用于函数式接口对应唯一抽象方法的实现

Lambda表达式的语法定义

Lambda语法的详细定义如下:

// en_US
InterfaceType interfaceObject = [Method Argument List] -> Method Body

// zh_CN
接口类型 接口实例 = [方法参数列表] -> 方法体
复制代码

更具体的描述应该是:

接口类型 接口实例临时变量 = (方法参数类型X 方法参数类型X临时变量 , 方法参数类型Y 方法参数类型Y临时变量...) -> { 方法体... return 接口抽象方法返回值对应类型类型实例;}

一个Lambda表达式由五个部分组成:

  • 返回值:接口类型以及接口类型对应的临时实例变量。
  • 等号:=
  • 方法参数列表:一般由中括号()包裹,格式是(类型1 类型1的临时变量,...,类型N 类型N的临时变量),在方法没有重载可以明确推断参数类型的时候,参数类型可以省略,只留下临时变量列表。特殊地,空参数列表用()表示,如果参数只有一个,可以省略()
  • 箭头:->
  • 方法体:一般由花括号{}包裹,格式是{方法逻辑... return 函数式接口方法返回值类型的值;},有几点需要注意:
    • 如果方法体是空实现,用{}表示,如Runnable runnable = () -> {};
    • 如果函数式接口抽象方法的返回值为void类型,则不需要return关键字语句,如Runnable runnable = () -> {int i=0; i++;};
    • 如果函数式接口抽象方法的方法体仅仅包含一个表达式,则不需要使用{}包裹,如Runnable runnable = () -> System.out.println("Hello World!");

举一些例子:

// Function - 具体
java.util.function.Function<String, Integer> functionY = (String string) -> {
    return Integer.parseInt(string);
};
// Function - 简化
java.util.function.Function<String, Integer> functionX = string -> Integer.parseInt(string);

// Runnable - 具体
Runnable runnableX = () -> {
    System.out.println("Hello World!");
};
// Runnable - 简化
Runnable runnableY = () -> System.out.println("Hello World!");

// 整数1-100的和 - 具体
int reduceX = IntStream.range(1, 101).reduce(0, (int addend, int augend) -> {
    return addend + augend;
});
// 整数1-100的和 - 简化
int reduceY = IntStream.range(1, 101).reduce(0, Integer::sum);
复制代码

目标类型与类型推断

先引入下面的一个场景:

// club.throwable.Runnable
@FunctionalInterface
public interface Runnable {

    void run();

    static void main(String[] args) throws Exception {
        java.lang.Runnable langRunnable = () -> {};
        club.throwable.Runnable customRunnable = () -> {};
        langRunnable.run();
        customRunnable.run();
    }
}
复制代码

笔者定义了一个和java.lang.Runnable完全一致的函数式接口club.throwable.Runnable,上面main()方法中,可以看到两个接口对应的Lambda表达式的方法体实现也是完全一致,但是很明显最终可以使用不同类型的接口去接收返回值,也就是这两个Lambda的类型是不相同的。而这两个Lambda表达式返回值的类型是我们最终期待的返回值类型(expecting a data type of XX),那么Lambda表达式就是对应的被期待的类型,这个被期待的类型就是Lambda表达式的目标类型

为了确定Lambda表达式的目标类型,Java编译器会基于对应的Lambda表达式,使用上下文或者场景进行综合推导,判断的一个因素就是上下文中对该Lambda表达式所期待的类型。因此,只能在Java编译器能够正确推断Lambda表达式目标类型的场景下才能使用Lambda表达式,这些场景包括:

  • 变量声明。
  • 赋值。
  • 返回语句。
  • 数组初始化器。
  • Lambda表达式函数体。
  • 条件表达式(condition ? processIfTrue() : processIfFalse())。
  • 类型转换(Cast)表达式。

Lambda表达式除了目标类型,还包含参数列表和方法体,而方法体需要依赖于参数列表进行实现,所以方法参数也是决定目标类型的一个因素。

方法参数的类型推导的过程主要依赖于两个语言特性:重载解析(Overload Resolution)和参数类型推导(Type Argument Inference)。

原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference

重载解析会为一个给定的方法调用(Method Invocation)寻找最合适的方法声明(Method Declaration)。由于不同的声明具有不同的签名,当Lambda表达式作为方法参数时,重载解析就会影响到Lambda表达式的目标类型。编译器会根据它对该Lambda表达式的所提供的信息的理解做出决定。如果Lambda表达式具有显式类型(参数类型被显式指定),编译器就可以直接使用Lambda表达式的返回类型;如果Lambda表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略Lambda表达式函数体而只依赖Lambda表达式参数的数量。

举个例子:

// 显式类型
Function<String, String> functionX = (String x) -> x;

// 隐式类型
Function<String, Integer> functionY = x -> Integer.parseInt(x);
复制代码

如果依赖于方法参数的类型推导最佳方法声明时存在二义性(Ambiguous),我们就需要利用转型(Cast)或显式Lambda表达式来提供更多的类型信息,从而Lambda表达式的目标类型。举个例子:

// 编译不通过
Object runnableX = () -> {};

// 编译通过 - Cast
Object runnableY = (Runnable) () -> {};


// 静态方法入参类型是函数式接口
public static void function(java.util.function.Function function) {

}

function((Function<String, Long>) (x) -> Long.parseLong(x));
复制代码

作用域

关于作用域的问题记住几点即可:

  • <1>Lambda表达式内的this引用和封闭类的this引用相同。
  • <2>Lambda表达式基于词法作用域,它不会从超类中继承任何变量,方法体里面的变量和它外部环境的变量具有相同的语义。
  • <3>Lambda expressions close over values, not variables,也就是Lambda表达式对值类型封闭,对变量(引用)类型开放(这一点正好解释了Lambda表达式内部引用外部的属性的时候,该属性必须定义为final)。

对于第<1>点举个例子:

public class LambdaThis {

    int x = 1;

    public void method() {
        Runnable runnable = () -> {
            int y = this.x;
            y++;
            System.out.println(y);
        };
        runnable.run();
    }

    public static void main(String[] args) throws Exception {
        LambdaThis lambdaThis = new LambdaThis();
        lambdaThis.method();   // 2
    }
}
复制代码

对于第<2>点举个例子:

public class LambdaScope {
    
    public void method() {
        int x = 1;
        Runnable runnable = () -> {
            // 编译不通过 - Lambda方法体外部已经定义了同名变量
            int x = 2;
        };
        runnable.run();
    }
}
复制代码

对于第<3>点举个例子:

public class LambdaValue {

    public void method() {
        (final) int x = 1;
        Runnable runnable = () -> {
            // 编译不通过 - 外部值类型使用了final
            x ++;
        };
        runnable.run();
    }
}

public class LambdaValue {

    public void method() {
        (final) IntHolder holder = new IntHolder();
        Runnable runnable = () -> {
            // 编译通过 - 使用了引用类型
            holder.x++;
        };
        runnable.run();
    }

    private static class IntHolder {

        int x = 1;
    }
}
复制代码

方法引用

方法引用(Method Reference)是一种功能和Lambda表达式类似的表达式,需要目标类型和实现函数式接口,但是这个实现形式并不是通过方法体,而是通过方法名称(或者关键字)关联到一个已经存在的方法,本质是编译层面的技术,旨在进一步简化Lambda表达式方法体和一些特定表达式的实现。方法引用的类型归结如下:

类型 例子
静态方法引用 ClassName::methodName
指定对象实例方法引用 instanceRef::methodName
特定类型任意对象方法引用 ContainingType::methodName
超类方法引用 supper::methodName
构造器方法引用 ClassName::new
数组构造器方法引用 TypeName[]::new

可见其基本形式是:方法容器::方法名称或者关键字

举一些基本的使用例子:

// 静态方法引用
public class StaticMethodRef {

    public static void main(String[] args) throws Exception {
        Function<String, Integer> function = StaticMethodRef::staticMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    }

    public static Integer staticMethod(String value) {
        return Integer.parseInt(value);
    }
}

// 指定对象实例方法引用
public class ParticularInstanceRef {

    public Integer refMethod(String value) {
        return Integer.parseInt(value);
    }

    public static void main(String[] args) throws Exception{
        ParticularInstanceRef ref = new ParticularInstanceRef();
        Function<String, Integer> function = ref::refMethod;
        Integer result = function.apply("10086");
        System.out.println(result);  // 10086
    }
}

// 特定类型任意对象方法引用
String[] stringArray = {"C", "a", "B"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
System.out.println(Arrays.toString(stringArray)); // [a, B, C]

// 超类方法引用
public class SupperRef {

    public static void main(String[] args) throws Exception {
        Sub sub = new Sub();
        System.out.println(sub.refMethod("10086")); // 10086
    }

    private static class Supper {

        private Integer supperRefMethod(String value) {
            return Integer.parseInt(value);
        }
    }

    private static class Sub extends Supper {

        private Integer refMethod(String value) {
            Function<String, Integer> function = super::supperRefMethod;
            return function.apply(value);
        }
    }
}

// 构造器方法引用
public class ConstructorRef {

    public static void main(String[] args) throws Exception {
        Function<String, Person> function = Person::new;
        Person person = function.apply("doge");
        System.out.println(person.getName()); // doge
    }

    private static class Person {

        private final String name;

        public Person(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

// 数组构造器方法引用
Function<Integer, Integer[]> function = Integer[]::new;
Integer[] array = function.apply(10);
System.out.println(array.length); // 10
复制代码

Java中Lambda的底层实现原理

重点要说三次:

  • Lambda表达式底层不是匿名类实现。
  • Lambda表达式底层不是匿名类实现。
  • Lambda表达式底层不是匿名类实现。

在深入学习Lambda表达式之前,笔者也曾经认为Lambda就是匿名类的语法糖:

// Lambda
Function<String, String> functionX = (String x) -> x;

// 错误认知
Function<String, String> functionX = new Function<String, String>() {
    @Override public Void apply(String x) {
        return x;
    }
}
复制代码

Lambda就是匿名类的语法糖这个认知是错误的。下面举一个例子,从源码和字节码的角度分析一下Lambda表达式编译和执行的整个流程。

public class Sample {

    public static void main(String[] args) throws Exception {
        Runnable runnable = () -> {
            System.out.println("Hello World!");
        };
        runnable.run();
        String hello = "Hello ";
        Function<String, String> function = string -> hello + string;
        function.apply("Doge");
    }
}
复制代码

添加VM参数-Djdk.internal.lambda.dumpProxyClasses=.运行上面的Sample#main()方法,项目根目录动态生成了两个类如下:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Sample$$Lambda$14 implements Runnable {
    private Sample$$Lambda$14() {
    }

    @Hidden
    public void run() {
        Sample.lambda$main$0();
    }
}

import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Function;

// $FF: synthetic class
final class Sample$$Lambda$15 implements Function {
    private final String arg$1;

    private Sample$$Lambda$15(String var1) {
        this.arg$1 = var1;
    }

    private static Function get$Lambda(String var0) {
        return new Sample$$Lambda$15(var0);
    }

    @Hidden
    public Object apply(Object var1) {
        return Sample.lambda$main$1(this.arg$1, (String)var1);
    }
}
复制代码

反查两个类的字节码,发现了类修饰符为final synthetic。接着直接看封闭类Sample的字节码:

public class club/throwable/Sample {
     <ClassVersion=52>
     <SourceFile=Sample.java>

     public Sample() { // <init> //()V
         <localVar:index=0 , name=this , desc=Lclub/throwable/Sample;, sig=null, start=L1, end=L2>

         L1 {
             aload0 // reference to self
             invokespecial java/lang/Object.<init>()V
             return
         }
         L2 {
         }
     }

     public static main(java.lang.String[] arg0) throws java/lang/Exception { //([Ljava/lang/String;)V
         <localVar:index=0 , name=args , desc=[Ljava/lang/String;, sig=null, start=L1, end=L2>
         <localVar:index=1 , name=runnable , desc=Lclub/throwable/Runnable;, sig=null, start=L3, end=L2>
         <localVar:index=2 , name=hello , desc=Ljava/lang/String;, sig=null, start=L4, end=L2>
         <localVar:index=3 , name=function , desc=Ljava/util/function/Function;, sig=Ljava/util/function/Function<Ljava/lang/String;Ljava/lang/String;>;, start=L5, end=L2>

         L1 {
             invokedynamic 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; : run()Lclub/throwable/Runnable; ()V club/throwable/Sample.lambda$main$0()V (6) ()V
             astore1
         }
         L3 {
             aload1
             invokeinterface club/throwable/Runnable.run()V
         }
         L6 {
             ldc "Hello " (java.lang.String)
             astore2
         }
         L4 {
             aload2
             invokedynamic 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; : apply(Ljava/lang/String;)Ljava/util/function/Function; (Ljava/lang/Object;)Ljava/lang/Object; club/throwable/Sample.lambda$main$1(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; (6) (Ljava/lang/String;)Ljava/lang/String;
             astore3
         }
         L5 {
             aload3
             ldc "Doge" (java.lang.String)
             invokeinterface java/util/function/Function.apply(Ljava/lang/Object;)Ljava/lang/Object;
             pop
         }
         L7 {
             return
         }
         L2 {
         }
     }

     private static synthetic lambda$main$1(java.lang.String arg0, java.lang.String arg1) { //(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
         <localVar:index=0 , name=hello , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>
         <localVar:index=1 , name=string , desc=Ljava/lang/String;, sig=null, start=L1, end=L2>

         L1 {
             new java/lang/StringBuilder
             dup
             invokespecial java/lang/StringBuilder.<init>()V
             aload0 // reference to arg0
             invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
             aload1
             invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
             invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
             areturn
         }
         L2 {
         }
     }

     private static synthetic lambda$main$0() { //()V
         L1 {
             getstatic java/lang/System.out:java.io.PrintStream
             ldc "Hello World!" (java.lang.String)
             invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
         }
         L2 {
             return
         }
     }
// The following inner classes couldn't be decompiled: java/lang/invoke/MethodHandles$Lookup 
}
复制代码

上面的字节码已经被Bytecode-Viewer工具格式化过,符合于人的阅读习惯,从字节码的阅读,结合前面的分析大概可以得出下面的结论:

  • <1>Lambda表达式在编译期通过字节码增强技术新增一个模板类实现对应的接口类型,这个模板类的所有属性都使用final修饰,模板类由关键字final synthetic修饰。
  • <2>:封闭类会基于类内的Lambda表达式类型生成private static synthetic修饰的静态方法,该静态方法的方法体就是来源于Lambda方法体,这些静态方法的名称是lambda$封闭类方法名$递增数字
  • <3>Lambda表达式调用最终通过字节码指令invokedynamic,忽略中间过程,最后调用到第<2>步中对应的方法。

限于篇幅问题,这里把Lambda表达式的底层原理做了简单的梳理(这个推导过程仅限于个人理解,依据尚未充分):

  • <1>:封闭类会基于类内的Lambda表达式类型生成private static synthetic修饰的静态方法,该静态方法的方法体就是来源于Lambda方法体,这些静态方法的名称是lambda$封闭类方法名$递增数字
  • <2>Lambda表达式会通过LambdaMetafactory#metafactory()方法,生成一个对应函数式接口的模板类,模板类的接口方法实现引用了第<1>步中定义的静态方法,同时创建一个调用点ConstantCallSite实例,后面会通过Unsafe#defineAnonymousClass()实例化模板类。。
  • <3>:调用点ConstantCallSite实例中的方法句柄MethodHandle会根据不同场景选取不同的实现,MethodHandle的子类很多,这里无法一一展开。
  • <4>:通过invokedynamice指令,基于第<1>步中的模板类实例、第<3>步中的方法句柄以及方法入参进行方法句柄的调用,实际上最终委托到第<1>步中定义的静态方法中执行。

如果想要跟踪Lambda表达式的整个调用生命周期,可以以LambdaMetafactory#metafactory()方法为入口开始DEBUG,调用链路十分庞大,需要有足够的耐心。总的来说就是:Lambda表达式是基于JSR-292引入的动态语言调用包java.lang.invokeUnsafe#defineAnonymousClass()定义的轻量级模板类实现的,主要用到了invokedynamice字节码指令,关联到方法句柄MethodHandle、调用点CallSite等相对复杂的知识点,这里不再详细展开。

实战

基于JdbcTemplate进行轻量级DAO封装

假设订单表的DDL如下:

CREATE TABLE `t_order`
(
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    create_time DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
    edit_time   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    user_id     BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    order_id    VARCHAR(64)     NOT NULL COMMENT '订单ID',
    amount      DECIMAL(12, 2)  NOT NULL DEFAULT 0 COMMENT '订单金额',
    INDEX idx_user_id (user_id),
    UNIQUE uniq_order_id (order_id)
) COMMENT '订单表';
复制代码

下面基于JdbcTemplate封装一个轻量级的OrderDao

// 辅助接口
@FunctionalInterface
public interface PreparedStatementProcessor {

    void process(PreparedStatement ps) throws SQLException;
}

@FunctionalInterface
public interface ResultSetConverter<T> {

    T convert(ResultSet resultSet) throws SQLException;
}

// OrderDao接口
public interface OrderDao {

    int insertSelective(Order record);

    int updateSelective(Order record);

    Order selectOneByOrderId(String orderId);

    List<Order> selectByUserId(Long userId);
}

// OrderDao实现
@Repository
@RequiredArgsConstructor
public class MySqlOrderDao implements OrderDao {

    private final JdbcTemplate jdbcTemplate;

    private static final ResultSetConverter<Order> CONVERTER = r -> {
        Order order = new Order();
        order.setId(r.getLong("id"));
        order.setCreateTime(r.getTimestamp("create_time").toLocalDateTime());
        order.setEditTime(r.getTimestamp("edit_time").toLocalDateTime());
        order.setUserId(r.getLong("user_id"));
        order.setAmount(r.getBigDecimal("amount"));
        order.setOrderId(r.getString("order_id"));
        return order;
    };

    private static final ResultSetExtractor<List<Order>> MULTI = r -> {
        List<Order> list = new ArrayList<>();
        while (r.next()) {
            list.add(CONVERTER.convert(r));
        }
        return list;
    };

    private static final ResultSetExtractor<Order> SINGLE = r -> {
        if (r.next()) {
            return CONVERTER.convert(r);
        }
        return null;
    };

    @Override
    public int insertSelective(Order record) {
        List<PreparedStatementProcessor> processors = new ArrayList<>();
        StringBuilder sql = new StringBuilder("INSERT INTO t_order(");
        Cursor cursor = new Cursor();
        if (null != record.getId()) {
            int idx = cursor.add();
            sql.append("id,");
            processors.add(p -> p.setLong(idx, record.getId()));
        }
        if (null != record.getOrderId()) {
            int idx = cursor.add();
            sql.append("order_id,");
            processors.add(p -> p.setString(idx, record.getOrderId()));
        }
        if (null != record.getUserId()) {
            int idx = cursor.add();
            sql.append("user_id,");
            processors.add(p -> p.setLong(idx, record.getUserId()));
        }
        if (null != record.getAmount()) {
            int idx = cursor.add();
            sql.append("amount,");
            processors.add(p -> p.setBigDecimal(idx, record.getAmount()));
        }
        if (null != record.getCreateTime()) {
            int idx = cursor.add();
            sql.append("create_time,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getCreateTime())));
        }
        if (null != record.getEditTime()) {
            int idx = cursor.add();
            sql.append("edit_time,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getEditTime())));
        }
        StringBuilder realSql = new StringBuilder(sql.substring(0, sql.lastIndexOf(",")));
        realSql.append(") VALUES (");
        int idx = cursor.idx();
        for (int i = 0; i < idx; i++) {
            if (i != idx - 1) {
                realSql.append("?,");
            } else {
                realSql.append("?");
            }
        }
        realSql.append(")");
        // 传入主键的情况
        if (null != record.getId()) {
            return jdbcTemplate.update(realSql.toString(), p -> {
                for (PreparedStatementProcessor processor : processors) {
                    processor.process(p);
                }
            });
        } else {
            // 自增主键的情况
            KeyHolder keyHolder = new GeneratedKeyHolder();
            int count = jdbcTemplate.update(p -> {
                PreparedStatement ps = p.prepareStatement(realSql.toString(), Statement.RETURN_GENERATED_KEYS);
                for (PreparedStatementProcessor processor : processors) {
                    processor.process(ps);
                }
                return ps;
            }, keyHolder);
            record.setId(Objects.requireNonNull(keyHolder.getKey()).longValue());
            return count;
        }
    }

    @Override
    public int updateSelective(Order record) {
        List<PreparedStatementProcessor> processors = new ArrayList<>();
        StringBuilder sql = new StringBuilder("UPDATE t_order SET ");
        Cursor cursor = new Cursor();
        if (null != record.getOrderId()) {
            int idx = cursor.add();
            sql.append("order_id = ?,");
            processors.add(p -> p.setString(idx, record.getOrderId()));
        }
        if (null != record.getUserId()) {
            int idx = cursor.add();
            sql.append("user_id = ?,");
            processors.add(p -> p.setLong(idx, record.getUserId()));
        }
        if (null != record.getAmount()) {
            int idx = cursor.add();
            sql.append("amount = ?,");
            processors.add(p -> p.setBigDecimal(idx, record.getAmount()));
        }
        if (null != record.getCreateTime()) {
            int idx = cursor.add();
            sql.append("create_time = ?,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getCreateTime())));
        }
        if (null != record.getEditTime()) {
            int idx = cursor.add();
            sql.append("edit_time = ?,");
            processors.add(p -> p.setTimestamp(idx, Timestamp.valueOf(record.getEditTime())));
        }
        StringBuilder realSql = new StringBuilder(sql.substring(0, sql.lastIndexOf(",")));
        int idx = cursor.add();
        processors.add(p -> p.setLong(idx, record.getId()));
        realSql.append(" WHERE id = ?");
        return jdbcTemplate.update(realSql.toString(), p -> {
            for (PreparedStatementProcessor processor : processors) {
                processor.process(p);
            }
        });
    }

    @Override
    public Order selectOneByOrderId(String orderId) {
        return jdbcTemplate.query("SELECT * FROM t_order WHERE order_id = ?", p -> p.setString(1, orderId), SINGLE);
    }

    @Override
    public List<Order> selectByUserId(Long userId) {
        return jdbcTemplate.query("SELECT * FROM t_order WHERE order_id = ?", p -> p.setLong(1, userId), MULTI);
    }

    private static class Cursor {

        private int idx;

        public int add() {
            idx++;
            return idx;
        }

        public int idx() {
            return idx;
        }
    }
}
复制代码

类似于Mybatis Generator,上面的DAO实现笔者已经做了一个简单的生成器,只要配置好数据源的连接属性和表过滤规则就可以生成对应的实体类和DAO类。

基于Optional进行VO设置值

// 假设VO有多个层级,每个层级都不知道父节点是否为NULL,如下
// - OrderInfoVo
//   - UserInfoVo
//     - AddressInfoVo
//        - address(属性)
// 假设我要为address属性赋值,那么就会产生箭头型代码。

// 常规方法
String address = "xxx";
OrderInfoVo o = ...;
if(null != o){
    UserInfoVo uiv = o.getUserInfoVo();
    if (null != uiv){
        AddressInfoVo aiv = uiv.getAddressInfoVo();
        if (null != aiv){
            aiv.setAddress(address);
        }
    }
}

// 使用Optional和Lambda
String address = "xxx";
OrderInfoVo o = ...;
Optional.ofNullable(o).map(OrderInfoVo::getUserInfoVo).map(UserInfoVo::getAddressInfoVo).ifPresent(a -> a.setAddress(address));
复制代码

小结

LambdaJava中一个香甜的语法糖,拥抱Lambda,拥抱函数式编程,笔者也经历过抗拒、不看好、上手和真香的过程,目前也大量使用StreamLambda,能在保证性能的前提下,尽可能简化代码,解放劳动力。时代在进步,Java也在进步,这是很多人活着和坚持编程事业的信念。

参考资料:

个人博客

(本文完 e-a-20200208 c-5-d)

关注下面的标签,发现更多相似文章
评论