Lambda表达式与函数式接口

目录

[toc]

Lambda表达式Java函数式接口

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

简述Lambda表达式

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

举个栗子:

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

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

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

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

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

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

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

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

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

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

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

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

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

1
var1 -> var1.doSomeThing();

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

1
Var1::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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

package com.main;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

public class MainFunc {

public class Test1 {
private Test2 test2;

public Test2 getTest2() {
return test2;
}

public void setTest2(Test2 test2) {
this.test2 = test2;
}
}

public class Test2 {
private List<Test3> test3;

public List<Test3> getTest3() {
return test3;
}

public void setTest3(List<Test3> test3) {
this.test3 = test3;
}
}

public class Test3 {
private String str;

public String getStr() {
return str;
}

public void setStr(String str) {
this.str = str;
}
}

@Test
public void test4() {
Test1 test1 = new Test1();
Test2 test2 = new Test2();
test1.setTest2(test2);
List<Test3> list = new ArrayList<>();
list.add(new Test3());
test2.setTest3(list);
String str = "123";
str.concat("456");
list.get(0).setStr(str);
Optional<String> opt = Optional.ofNullable(test1).map(new Function<Test1, Test2>() {
@Override
public Test2 apply(Test1 test1) {
return test1.getTest2();
}
}).map(new Function<Test2, List<Test3>>() {
@Override
public List<Test3> apply(Test2 test2) {
return test2.getTest3();
}
}).map(new Function<List<Test3>, Test3>() {
@Override
public Test3 apply(List<Test3> list) {
return list != null && list.size() > 0 ? list.get(0) : null;
}
}).map(new Function<Test3, String>() {
@Override
public String apply(Test3 test3) {
return test3.getStr();
}
});
if (opt.isPresent()) {
System.out.println(opt.get());
}
}
}

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

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

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

最后是极致简写方法:

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

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

Java中Lambda的实现原理

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

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

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

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

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

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表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.main;

import org.junit.Test;

public class MainFunc {

@FunctionalInterface
private interface FI<N> {
void run(N n);
};

private void showFi(FI<String> n) {
n.run("showFi");
}

@Test
public void test4() {
showFi(s -> System.out.println(s));
}
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public void test4();
descriptor: ()V
flags: ACC_PUBLIC
RuntimeVisibleAnnotations:
0: #16()
Code:
stack=4, locals=6, args_size=1
0: new #17 // class com/main/MainFunc$Test1
3: dup
4: aload_0
5: invokespecial #19 // Method com/main/MainFunc$Test1."<init>":(Lcom/main/MainFunc;)V
8: astore_1
9: new #22 // class com/main/MainFunc$Test2
12: dup
13: aload_0
14: invokespecial #24 // Method com/main/MainFunc$Test2."<init>":(Lcom/main/MainFunc;)V
17: astore_2
18: aload_1
19: aload_2
20: invokevirtual #25 // Method com/main/MainFunc$Test1.setTest2:(Lcom/main/MainFunc$Test2;)V
23: new #29 // class java/util/ArrayList
26: dup
27: invokespecial #31 // Method java/util/ArrayList."<init>":()V
30: astore_3
31: aload_3
32: new #32 // class com/main/MainFunc$Test3
35: dup
36: aload_0
37: invokespecial #34 // Method com/main/MainFunc$Test3."<init>":(Lcom/main/MainFunc;)V
40: invokeinterface #35, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
45: pop
46: aload_2
47: aload_3
48: invokevirtual #41 // Method com/main/MainFunc$Test2.setTest3:(Ljava/util/List;)V
51: ldc #45 // String 123
53: astore 4
55: aload 4
57: ldc #47 // String 456
59: invokevirtual #49 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
62: pop
63: aload_3
64: iconst_0
65: invokeinterface #55, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
70: checkcast #32 // class com/main/MainFunc$Test3
73: aload 4
75: invokevirtual #59 // Method com/main/MainFunc$Test3.setStr:(Ljava/lang/String;)V
78: aload_1
79: invokestatic #63 // Method java/util/Optional.ofNullable:(Ljava/lang/Object;)Ljava/util/Optional;
82: invokedynamic #72, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
87: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
90: invokedynamic #77, 0 // InvokeDynamic #1:apply:()Ljava/util/function/Function;
95: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
98: invokedynamic #78, 0 // InvokeDynamic #2:apply:()Ljava/util/function/Function;
103: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
106: invokedynamic #79, 0 // InvokeDynamic #3:apply:()Ljava/util/function/Function;
111: invokevirtual #73 // Method java/util/Optional.map:(Ljava/util/function/Function;)Ljava/util/Optional;
114: astore 5
116: aload 5
118: invokevirtual #80 // Method java/util/Optional.isPresent:()Z
121: ifeq 138
124: getstatic #84 // Field java/lang/System.out:Ljava/io/PrintStream;
127: aload 5
129: invokevirtual #90 // Method java/util/Optional.get:()Ljava/lang/Object;
132: checkcast #50 // class java/lang/String
135: invokevirtual #93 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
138: return

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

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

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

请我喝杯咖啡吧~

支付宝
微信