Lambda表达式与函数式接口

目录

[toc]

Lambda表达式Java函数式接口

都9012年了,这篇博文代码看看5000年前4012年发布的Java 8新引入的Lambda表达式到底是个什么鬼。

简述Lambda表达式

Lambda表达式并不是Java 8特有的特性,其设计初衷是用于一些特定代码中,已知固定入参和固定返回值的时候,动态生成的一种函数。

举个栗子:

1public Var3 func(Var1 var1, Var2 var2) {
2Var3 var3 = doSomeThing(var1, var2);
3return var3;
4}

上述代码是我们常见的Java代码格式,假如说我们已经设定这个函数只会被在特定位置被调用,或者换种说法,我们假设func函数仅仅只会作为func2(Var3 var3)的入参。同时如果我们把func这个函数的声明放到func2的入参时声明,那此时我们此时调用时,其实这个函数是叫func还是叫funcA还是叫ABCD已经无所谓了。

因此我们就可以省略这个函数名,将其替换成->,由此将上述函数省略为:

1Var3 (Var1 var1, Var2 var2) -> {
2Var3 var3 = doSomeThing(var1, var2);
3return var3;
4}

接下来由于我们已知func2(Var3 var3)的入参肯定为Var3类型,所以上述代码又可以进一步省略:

1(Var1 var1, Var2 var2) -> {
2Var3 var3 = doSomeThing(var1, var2);
3return var3;
4}

同理由于我们已知func的入参类型肯定为Var1Var2,于是我们继续省略:

1(var1, var2) -> {
2Var3 var3 = doSomeThing(var1, var2);
3return var3;
4}

然后由于我们如果整个函数内部只有一行操作的话,则可以知道这个操作的返回值肯定是这一行的操作结果,因此我们继续省略:

1(var1, var2) -> doSomeThing(var1, var2);

最后,假如我们初始函数是:

1Var3 (Var1 var1) -> {
2return var1.doSomeThing();
3}

在我们简写成上述常见的表达式之后:

1var1 -> var1.doSomeThing();

由于我们知道入参只有一个,并且操作就是调用这个参数的一个子方法,而且这个入参叫做var1还是ABCD都无所谓,这个时候我们就可以极致缩写:

1Var1::doSomeThing()

意味着对这个表达式的入参直接调用Var1类的doSomeThing,然后将结果返回。

到此,整个Lambda写法的产生原因我们就已经知道了,不理解的可以重新回看整个缩写过程。

注意这里的推理过程是所有支持Lambda表达式的开发语言通用的精简思路,之后切换语言遇到Lambda表达式,我们需要用同样的思路面对。

实不相瞒,上述简化过程我就是在Python教程中知道的。

Java中Lambda表达式的好处

在我们Java 8之前,我们可能暂时还没有体会到Lambda表达式的好处,但是Java 8新引入的StreamOptional这两个类,让Java 8引入Lambda表达式成为一种趋势。

继续举个栗子,假如我们现在有一个多层级的对象,我们需要获取其最底层的一个字段时,使用Optional类可以比较方便的判定,相关教程见我另一个帖子《Optional工具类》

这里我们先不要发散,假如我们使用Optional取一个中间可能存在null的多层级对象时,假设我们现在还不知道Lambda表达式这个东西,而是单纯使用Optional所有方法提供的入参直接暴力实现,那么最后的代码如下:

 1
 2package com.main;
 3
 4import org.junit.Test;
 5
 6import java.util.ArrayList;
 7import java.util.List;
 8import java.util.Optional;
 9import java.util.function.Consumer;
10import java.util.function.Function;
11
12public class MainFunc {
13
14    public class Test1 {
15        private Test2 test2;
16
17        public Test2 getTest2() {
18            return test2;
19        }
20
21        public void setTest2(Test2 test2) {
22            this.test2 = test2;
23        }
24    }
25
26    public class Test2 {
27        private List<Test3> test3;
28
29        public List<Test3> getTest3() {
30            return test3;
31        }
32
33        public void setTest3(List<Test3> test3) {
34            this.test3 = test3;
35        }
36    }
37
38    public class Test3 {
39        private String str;
40
41        public String getStr() {
42            return str;
43        }
44
45        public void setStr(String str) {
46            this.str = str;
47        }
48    }
49
50    @Test
51    public void test4() {
52        Test1 test1 = new Test1();
53        Test2 test2 = new Test2();
54        test1.setTest2(test2);
55        List<Test3> list = new ArrayList<>();
56        list.add(new Test3());
57        test2.setTest3(list);
58        String str = "123";
59        str.concat("456");
60        list.get(0).setStr(str);
61        Optional<String> opt = Optional.ofNullable(test1).map(new Function<Test1, Test2>() {
62            @Override
63            public Test2 apply(Test1 test1) {
64                return test1.getTest2();
65            }
66        }).map(new Function<Test2, List<Test3>>() {
67            @Override
68            public List<Test3> apply(Test2 test2) {
69                return test2.getTest3();
70            }
71        }).map(new Function<List<Test3>, Test3>() {
72            @Override
73            public Test3 apply(List<Test3> list) {
74                return list != null && list.size() > 0 ? list.get(0) : null;
75            }
76        }).map(new Function<Test3, String>() {
77            @Override
78            public String apply(Test3 test3) {
79                return test3.getStr();
80            }
81        });
82        if (opt.isPresent()) {
83            System.out.println(opt.get());
84        }
85    }
86}

整段代码中,由于我们按照Optional.map的要求,通过实现Fuction接口函数,并且重写其apply函数,从而实现业务诉求。但是这种实现方式阅读费力,并且撰写辛苦,而所有map中实质的操作却仅仅只有一行,这种代码既不优雅,也不是我们引入Optional的初衷。

在这种情况下,我们使用Lambda表达式替代上述代码中test4的实现:

 1    @Test
 2    public void test4() {
 3        Test1 test1 = new Test1();
 4        Test2 test2 = new Test2();
 5        test1.setTest2(test2);
 6        List<Test3> list = new ArrayList<>();
 7        list.add(new Test3());
 8        test2.setTest3(list);
 9        String str = "123";
10        str.concat("456");
11        list.get(0).setStr(str);
12        if (opt.isPresent()) {
13            System.out.println(opt.get());
14        }
15        Optional<String> opt = Optional.ofNullable(test1)
16                .map(t1 -> t1.getTest2())
17                .map(t2 -> t2.getTest3())
18                .map(t3List -> t3List != null && t3List.size() > 0 ? t3List.get(0) : null)
19                .map(t3 -> t3.getStr());
20        if (opt.isPresent()) {
21            System.out.println(opt.get());
22        }
23    }

最后是极致简写方法:

 1    @Test
 2    public void test4() {
 3        Test1 test1 = new Test1();
 4        Test2 test2 = new Test2();
 5        test1.setTest2(test2);
 6        List<Test3> list = new ArrayList<>();
 7        list.add(new Test3());
 8        test2.setTest3(list);
 9        String str = "123";
10        str.concat("456");
11        list.get(0).setStr(str);
12        if (opt.isPresent()) {
13            System.out.println(opt.get());
14        }
15        Optional<String> opt = Optional.ofNullable(test1)
16                .map(Test1::getTest2)
17                .map(Test2::getTest3)
18                .map(test3 -> test3 != null && test3.size() > 0 ? test3.get(0) : null)
19                .map(Test3::getStr);
20        if (opt.isPresent()) {
21            System.out.println(opt.get());
22        }
23    }

整个实现上瞬间清爽很多,并且代码量非常少。

Java中Lambda的实现原理

其实在上述代码优化过程中,从最开始直接在map方法中实现函数接口,到直接替换成Lambda表达式,我们省略了一个推导步骤:

首先尝试将函数接口的实现抽出去:

 1    @Test
 2    public void test4() {
 3        Test1 test1 = new Test1();
 4        Test2 test2 = new Test2();
 5        test1.setTest2(test2);
 6        List<Test3> list = new ArrayList<>();
 7        list.add(new Test3());
 8        test2.setTest3(list);
 9        String str = "123";
10        str.concat("456");
11        list.get(0).setStr(str);
12        Function<Test1, Test2> func1 = new Function<Test1, Test2>() {
13            @Override
14            public Test2 apply(Test1 test1) {
15                return test1.getTest2();
16            }
17        };
18        Function<Test2, List<Test3>> func2 = new Function<Test2, List<Test3>>() {
19            @Override
20            public List<Test3> apply(Test2 test2) {
21                return test2.getTest3();
22            }
23        };
24        Function<List<Test3>, Test3> func3 = new Function<List<Test3>, Test3>() {
25            @Override
26            public Test3 apply(List<Test3> list) {
27                return list != null && list.size() > 0 ? list.get(0) : null;
28            }
29        };
30        Function<Test3, String> func4 = new Function<Test3, String>() {
31            @Override
32            public String apply(Test3 test3) {
33                return test3.getStr();
34            }
35        };
36        Optional<String> opt = Optional.ofNullable(test1).map(func1).map(func2).map(func3).map(func4);
37        if (opt.isPresent()) {
38            System.out.println(opt.get());
39        }
40    }

然后我们有已经知道map中实际是可以直接填Lambda表达式的,这里我们尝试将Lambda表达式赋值给func1、func2、func3、func4:

 1    @Test
 2    public void test4() {
 3        Test1 test1 = new Test1();
 4        Test2 test2 = new Test2();
 5        test1.setTest2(test2);
 6        List<Test3> list = new ArrayList<>();
 7        list.add(new Test3());
 8        test2.setTest3(list);
 9        String str = "123";
10        str.concat("456");
11        list.get(0).setStr(str);
12        Function<Test1, Test2> func1 = Test1::getTest2;
13        Function<Test2, List<Test3>> func2 = Test2::getTest3;
14        Function<List<Test3>, Test3> func3 = (l) -> l != null && l.size() > 0 ? l.get(0) : null;
15        Function<Test3, String> func4 = Test3::getStr;
16        Optional<String> opt = Optional.ofNullable(test1).map(func1).map(func2).map(func3).map(func4);
17        if (opt.isPresent()) {
18            System.out.println(opt.get());
19        }
20    }

OK,到这里,我们可以发现发现所谓Lambda表达式,实际上就是自己帮你实现了一个函数式接口而已,这部分实现过程由Java 8之前你来完成,优化到了编译器自己完成,从而实现了代码上的优雅。

这里我们引申一下,在Java中,函数接口有 3 条重要法则:

  • 一个函数接口只有一个抽象方法。
  • 在 Object 类中属于公共方法的抽象方法不会被视为单一抽象方法。
  • 函数接口可以有默认方法和静态方法。

任何满足单一抽象方法法则的接口,都会被自动视为函数接口。这包括 Runnable 和 Callable 等传统接口,以及您自己构建的自定义接口。

上述三原则引用自《Java 8 习惯用语,第 7 部分 —— 函数接口》

接下来,让我开始尝试看看Java编译器(JDK)再给我们编译Lambda表达式时做了哪些优化动作。

Java1.8引入的新函数式接口

所有函数式接口见java.util.function包,这里只挑取几个典型的。

Consumer接口:提供一个void accept(T t);函数,一般我们的函数只有一个入参,没有返回值时,可以实现该接口

Function接口:提供一个R apply(T t);函数,一般我们只有一个入参,同时有返回值时,可以实现该接口,标准的最常用的函数式接口

Predicate接口:提供一个boolean test(T t);函数,一般我们需要对入参做一些判断时,可以实现该接口,Stream.filter的入参就是该接口的实现类。

Supplier接口:提供一个T get();函数,如果函数没有入参,只有返回值,譬如我们的JavaBean中的get方法,可以实现该接口。

Java编译器自动优化实现函数接口

由上我们已知,其实Java 8中带来的Lambda表达式,就是一种能够减少我们实现接口函数的语法糖,Java能够通过我们的返回值,讲一个Lambda表达式合理的转换成一个函数接口的实现。

在了解其本质之后,我们甚至可以自己定义一个接口函数用于接收一个Lambda表达式:

 1package com.main;
 2
 3import org.junit.Test;
 4
 5public class MainFunc {
 6
 7    @FunctionalInterface
 8    private interface FI<N> {
 9        void run(N n);
10    };
11
12    private void showFi(FI<String> n) {
13        n.run("showFi");
14    }
15
16    @Test
17    public void test4() {
18        showFi(s -> System.out.println(s));
19    }
20}

java编译器的实现是一种动态实现,不受函数接口的接口名或者其抽象方法的名称的限制,由此我们也说java中的Lambda表达式是一种动态语言类型。

Java编译器对动态函数的优化

如果我们希望一窥JDK8在编译过程中,如何实现通过阅读反编译之后的class的代码进行查阅,指令为javap -v -p YourClass.class > yourRecordFile,整个函数的编译结果如下:

 1public void test4();
 2descriptor: ()V
 3flags: ACC_PUBLIC
 4RuntimeVisibleAnnotations:
 50: #16()
 6Code:
 7stack=4, locals=6, args_size=1
 80: new #17 // class com/main/MainFunc$Test1
 93: dup
104: aload_0
115: invokespecial #19 // Method com/main/MainFunc$Test1."<init>":(Lcom/main/MainFunc;)V
128: astore_1
139: new #22 // class com/main/MainFunc$Test2
1412: dup
1513: aload_0
1614: invokespecial #24 // Method com/main/MainFunc$Test2."<init>":(Lcom/main/MainFunc;)V
1717: astore_2
1818: aload_1
1919: aload_2
2020: invokevirtual #25 // Method com/main/MainFunc$Test1.setTest2:(Lcom/main/MainFunc$Test2;)V
2123: new #29 // class java/util/ArrayList
2226: dup
2327: invokespecial #31 // Method java/util/ArrayList."<init>":()V
2430: astore_3
2531: aload_3
2632: new #32 // class com/main/MainFunc$Test3
2735: dup
2836: aload_0
2937: invokespecial #34 // Method com/main/MainFunc$Test3."<init>":(Lcom/main/MainFunc;)V
3040: invokeinterface #35, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
3145: pop
3246: aload_2
3347: aload_3
3448: invokevirtual #41 // Method com/main/MainFunc$Test2.setTest3:(Ljava/util/List;)V
3551: ldc #45 // String 123
3653: astore 4
3755: aload 4
3857: ldc #47 // String 456
3959: invokevirtual #49 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
4062: pop
4163: aload_3
4264: iconst_0
4365: invokeinterface #55, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
4470: checkcast #32 // class com/main/MainFunc$Test3
4573: aload 4
4675: invokevirtual #59 // Method com/main/MainFunc$Test3.setStr:(Ljava/lang/String;)V
4778: aload_1
4879: invokestatic #63 // Method java/util/Optional.ofNullable:(Ljava/lang/Object;)Ljava/util/Optional;
4982: invokedynamic #72, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
5087: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
5190: invokedynamic #77, 0 // InvokeDynamic #1:apply:()Ljava/util/function/Function;
5295: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
5398: invokedynamic #78, 0 // InvokeDynamic #2:apply:()Ljava/util/function/Function;
54103: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
55106: invokedynamic #79, 0 // InvokeDynamic #3:apply:()Ljava/util/function/Function;
56111: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
57114: astore 5
58116: aload 5
59118: invokevirtual #80 // Method java/util/Optional.isPresent:()Z
60121: ifeq 138
61124: getstatic #84 // Field java/lang/System.out:Ljava/io/PrintStream;
62127: aload 5
63129: invokevirtual #90 // Method java/util/Optional.get:()Ljava/lang/Object;
64132: checkcast #50 // class java/lang/String
65135: invokevirtual #93 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66138: return

此时我们就不难发现,其实这里我们的4个lambda表达式编译结果对应的字节码指令为invokedynamic,这也就意味着在我们只有将lambda表达式的返回值赋值给一个函数接口的时候,他的类型才能够给动态识别,由此实现了lambda表达式的动态绑定。

由此lambda的外衣也就扒的差不多了,如果文中有什么表达或者理解错误的,欢迎指正。