学习Java函数式编程(一):初探

Java 8开始引入了Lambda表达式,第一次使用的时候,我是感动的,真的太好用了。接下来将做些笔记,以期了解Java函数式编程。

快速入手

流(Stream)是什么?这里有一篇文章(《Java 8中的Streams API详解》)给出了解释。这里先不纠结抽象的概念,而是看看具体的代码操作,本人认为,对一个Java程序员来说,Lambda表达式中的()和{}是需要区分的内容。

Lambda中() & {}

首先看一个Filter的例子:

1
2
3
4
5
6
7
8
9
public class FilterDemo {
public static void main(String[] args) {
List<String> beginningWithNumbers = Stream.of("a", "1asd", "5asds")
.filter(string -> Character.isDigit(string.charAt(0)))
.collect(Collectors.toList());
System.out.println(beginningWithNumbers);
}
}

Lambda是采用的 T -> Function -> R 的形式,string -> Character.isDigit(string.charAt(0))也可以添加()写成string -> (Character.isDigit(string.charAt(0)))。

如果更换成{},则需要显式的写return语句,就像正常书写方法一样,打印下面的语句:

1
2
3
4
5
6
7
8
9
10
System.out.println(Stream.of("a", "1asd", "5asds")
.filter(string -> {
System.out.println("sdsdsss");
return Character.isDigit(string.charAt(0));
}));
System.out.println(Stream.of("a", "1asd", "5asds")
.filter(string ->
Character.isDigit(string.charAt(0))
));

输出为:

1
2
3
4
java.util.stream.ReferencePipeline$2@12edcd21
java.util.stream.ReferencePipeline$2@52cc8049
Process finished with exit code 0

这段输出中,一开始没有打印出”sdsdsss”,说明这不是一个执行过程,而更像是声明方法刻画Stream。

要执行”sdsdsss”,需要采用及早求值方法(与之相反的概念叫惰性求值方法,是上一句提到的“声明”和”刻画”的方法):

1
2
3
4
5
System.out.println(Stream.of("a", "1asd", "5asds")
.filter(string -> {
System.out.println("sdsdsss");
return Character.isDigit(string.charAt(0));
}).collect(Collectors.toList()));

输出结果:

1
2
3
4
sdsdsss
sdsdsss
sdsdsss
[1asd, 5asds]

下图模拟了执行过程:

函数式编程01

与树的遍历做比较:形式上,不那么严谨的说,很类似广度优先遍历,而一般的for循环更似深度优先遍历。

重构代码

本小节将采用前文所述方式重构一个多层for循环代码。

将要给出的实例中,for循环所处的场景和循环逻辑是这样的:现在某学校有5个班,id分别为1、2、3、4、5,每个班有三名学生,将所属班级id为奇数且名称不叫小明的学生的姓名,全部打印出来。代码如下:

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
public class OldForDemo {
public static void main(String[] args) {
// 每个数组包括各个班级的学生名
String[] s1 = {"小明", "A01", "A02"};
String[] s2 = {"小张", "B01", "B02"};
String[] s3 = {"小明", "C01", "C02"};
String[] s4 = {"小陈", "D01", "D02"};
String[] s5 = {"小农", "E01", "E02"};
// 班级c01对象
Clazz c01 = new Clazz(1, s1);
// 班级c02对象
Clazz c02 = new Clazz(2, s2);
// 班级c03对象
Clazz c03 = new Clazz(3, s3);
// 班级c04对象
Clazz c04 = new Clazz(4, s4);
// 班级c05对象
Clazz c05 = new Clazz(5, s5);
// 用一个数组将所有班级对象打包在一起
Clazz[] clazzes = {c01, c02, c03, c04, c05};
List<String> list = new ArrayList<>();
// 将要被改造的旧循环逻辑
for (Clazz c : clazzes) {
if (c.getId() % 2 == 1) {
for (String s : c.getS()) {
if (!s.equals("小明")){
list.add(s);
}
}
}
}
// 遍历并打印最终构造的列表
for (String s : list) {
System.out.print(s + " ");
}
}
// 班级类
public static class Clazz {
private int id;
private String[] s;
public Clazz(int id, String[] strings) {
this.id = id;
this.s = strings;
}
public int getId() {
return id;
}
public String[] getS() {
return s;
}
}
}

代码各主要部分都进行了注释,逻辑也比较简单。

如何改造这段代码?我的方法是,视一次执行流程操作的对象对应一个Stream元素,首先看第一句for (Clazz c : clazzes),一次执行流程是的操作对象是c,那么该语句转化为:

1
Arrays.stream(clazzes)

第二句是if (c.getId() % 2 == 1),这里显然可以采用一个filter方法:

1
.filter(x -> x.getId() % 2 == 1)

第三句是for (String s : c.getS()),一次执行流程的操作对象是s,而s代表每个班级中的一个学生名,所以之前以班级对象对应一个Stream元素来构造Stream的方式,应该改为以班级中单个学生名对应一个Stream元素来构造Stream的方式。这里采用flatMap方法:

1
.flatMap(clazz -> Arrays.stream(clazz.getS()))

接着还要进行一次filter(),排除叫”小明”的名字,最后进行收集,这一部分完整代码如下:

1
2
3
4
5
List<String> l = Arrays.stream(clazzes)
.filter(x -> x.getId() % 2 == 1)
.flatMap(clazz -> Arrays.stream(clazz.getS()))
.filter(x -> !x.equals("小明"))
.collect(Collectors.toList());

输出为:

1
[A01, A02, C01, C02, 小农, E01, E02]

通过本次重构,代码逻辑清新了很多。

小结

本篇小文没有讲解map、reduce等高级函数如何使用,具体用法需自行查阅,但比较了lambda表达式中()与{}的区别,最后通过一个重构实例对函数式编程进行了实践。

参考

《Java 8函数式编程》

廖雪峰的官方网站:这里有对reduce函数的介绍,比较清晰;