学习Java函数式编程(四):使用Lambda重构代码

Lambda真是极好的!使用Lambda先从重构旧代码开始。《Java 8实战》对其进行了讲解,本章将按照此书的脉络进行组织,对相关章节进行复习总结。

改善代码的可读性

利用Lambda表达式可以写出更简洁、更灵活的代码。

匿名类 PK Lambda

在很多场景中,匿名类都能被Lambda取代,但在这之前,需要了解两者之间的三点不同。

this & super

在匿名类中,this代表的是类本身,而在Lambda中,代表的是宿主类。

字段隐藏

我们都知道,Java的动态分派体现在方法的调用上,字段不存在“重写”只能靠隐藏。如果在一个方法中出现下列代码:

1
2
3
4
5
int a = 10;
Runnable r1 = () -> {
int a = 2;
int b = 3;
};

编译器就会报错:Variable ‘a’ is already defined in the scope,而在匿名类中不会出现这样的问题,所以Lambda不能做字段隐藏。

方法重载

方法重载时,Lambda会遇到引用不明的问题,看下面的代码,它构造了一个函数式接口,并添加两个重载方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void doSomeThing(Runnable r) {
System.out.println("Runnable");
}
public static void doSomeThing(Task t) {
System.out.println("Task");
}
@FunctionalInterface
public static interface Task {
public void todo();
}

在main方法中调用doSomeThing(() -> System.out.println());编译器会提示说对doSomeThing的引用不明确,解决办法是采用显示类型转换:doSomeThing((Task) () -> System.out.println());

转换为方法引用

方法引用可以增加代码的可读性,能直观的表达代码的意图,格式为[类名]::[方法名]。

尽量考虑静态辅助方法,比如comparing、maxBy,这些方法设计之初就考虑了会结合方法引用一起使用。

增加代码的灵活性

行为参数化可以帮助程序员淡定的面对需求变化,这里介绍两种通用模式:有条件执行和环绕执行。

有条件延迟执行

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
public class LambdaTestDemo04 {
public static void main(String[] args) {
int num01 = getNumber01(x -> {
System.out.println("Lambda");
return x + x;
}, 3);
System.out.println("getNumber01: " + num01);
int num02 = getNumber02(getNumber03(1), 3);
System.out.println("getNumber02: " + num02);
}
public static int getNumber01(Func<Integer> func, int x) {
if (x < 3)
return func.get(x);
return -1;
}
public static int getNumber02(int t, int x) {
if (x < 3)
return t + x;
return -2;
}
public static int getNumber03(int x) {
System.out.println("getNumber03");
return x + x;
}
@FunctionalInterface
public static interface Func<Integer> {
int get(int x);
}
}

getNumber01和getNumber02是两个静态方法,它们是参数传入的方法。getNumber01中传入的是Lambda表达式,表达式描述的方法包含一个输出语句System.out.println("Lambda");,目的是检测它是否在作为参数传入时就执行;getNumber02中传入的是一个普通的静态方法,该方法返回一个int值,同时其方法体内也包含一个输出语句System.out.println("getNumber03");

由于初始条件的限制,getNumber01和getNumber02在运行的过程中皆不执行if内的return语句,结果如下:

1
2
3
4
5
getNumber01: -1
getNumber03
getNumber02: -2
Process finished with exit code 0

Lambda表达式没有执行方法内的打印语句,而普通静态类执行了内部的打印语句,此结果验证了Lambda是有条件的延迟执行。

《Java 8实战》一书中举了一个日志查询的例子,用实例分析了Lambda表达式延迟执行的各种好处,比如更易读封装行更好、满足条件才真正执行方法等。用重载构造log方法提供了一个改造旧代码的范例。

环绕执行

环绕执行的具体实现,和前文用重载构造log的实现异曲同工。在书中列举了一个多业务代码“共享”一套准备和清理的例子,这里采用了try-with-resource。

首先当然是函数式接口的创建:

1
2
3
4
@FunctionalInterface
public static interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

然后生成一个静态的执行方法:

1
2
3
4
5
public static String processFile(BufferedReaderProcessor b) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return b.process(br);
}
}

接下来就可以通过Lambda表达式实现各种业务逻辑,比如下面的代码中处理了两行:

1
String oneLine = processFile(x -> x.readLine() + x.readLine());

以上就是一个环绕执行的简单实现。

小结

本文首先比较了匿名类和Lambda,然后介绍了两种通用的重构模式,有条件延迟执行模式和环绕模式。