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
的入参类型肯定为Var1
和Var2
,于是我们继续省略:
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新引入的Stream
和Optional
这两个类,让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的外衣也就扒的差不多了,如果文中有什么表达或者理解错误的,欢迎指正。