一文速通lambda与函数式编程
# 写在文章开头
偶然看到以前写过一篇关于lambda的文章,表述的略带晦涩,所以打算重新将这篇文章整理分享,希望对你有所帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 快速了解lambda表达式
# 快速开始
我们希望希望对苹果集合按照重量进行排序,对此我们继承Comparable实现苹果类的排序方法compareTo:
@Data
@AllArgsConstructor
public class Apple implements Comparable<Apple> {
private int weight;
@Override
public int compareTo(Apple o) {
return this.weight - o.weight;
}
}
2
3
4
5
6
7
8
9
10
11
在Java8以前声明比较器通常是采用这种匿名内部类的方式,仅仅因为实现一个方法而显示声明了一长串的定义:
public static void main(String[] args) {
List<Apple> apples = Arrays.asList(new Apple(313),
new Apple(3213),
new Apple(7),
new Apple(8));
//java8以前的比较器声明
Comparator<Apple> comparator = new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.compareTo(a2);
}
};
//排序并输出
apples.sort(comparator);
apples.forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java8之后提出了函数式编程,对应的Comparator被声明为函数时接口,这一点我们从源码中看到FunctionalInterface这个注解,其内部也只有compare方法是作为抽象方法使用,这意味着我们可以针对compare写出一条精简的lambda表达式让编译器通过类型推断完成类型检查。
@FunctionalInterface
public interface Comparator<T> {
//......
int compare(T o1, T o2);
//......
}
2
3
4
5
6
这就意味着我们的表达式能够让编译器知晓对应的参数和返回值,即写出对应的lambda表达式,按照Java8的要求,我们的lambda表达式基本格式为参数列表 箭头 lambda代码主体:

于是我们就有了下面这样一段代码,即略去没必要的类声明和方法名,直接给出参数列表 箭头和比较逻辑的实现:
Comparator<Apple> comparator=(Apple a1, Apple a2)-> {return a1.compareTo(a2);};
当然上述的声明还不是最精简的,因为我们的逻辑仅仅为一行代码,按照lambda的语法糖我们完全可以略去return和花括号,又因为类型推断的机制,我们还可以略去参数列表的类型:
Comparator<Apple> comparator2=(a1,a2)-> a1.compareTo(a2);
上文我们提到一个类型推断概念,这里笔者也对此进行相应的补充,由Comparator是函数式接口以及其抽象方法compare方法可知,该函数式接口的函数签名为接收一个两个Apple类作为参数返回一个整型的表达式,由此我们的精简后的表达式按照列表进行代入推理也可以通过,由此类型检查通过,该表达式可用:

这里笔者也补充一条,对于Comparator实际上已经内置了comparing方法,我们可以结合函数时编程的规律得出下面这样一条表达式,这一点笔者会在后续函数式编程的文章展开介绍,所以就不再本篇文章中展开了:
Comparator<Apple> comparator3=Comparator.comparing(Apple::getWeight);
由此我们可以得出lambda表达式的优势:
- 匿名:使用
lambda声明方法无需像普通方法需要很多明确的东西,它给了我们一种写得少但是想得多的优雅。 - 函数:
lambda表达式用起来就和方法一样,有参数列表、函数主体、返回值等。 - 传递:
lambda可以作为参数传递或者存储在变量中。 - 简洁:使用
lambda可以避免没必要的模板编写。
通过一个示例简单了解了一下lambda表达式,不妨看看以下那几个lambda表达式是错误的:
(1) () -> {}
(2) () -> "Raoul"
(3) () -> {return "Mario";}
(4) (Integer i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}
2
3
4
5
这里笔者直接公布结果:
问题1是正确的,它代表无参且无返回值的`lambda`,它完全可以作为`runnable`这个接口的表达式`Runnable r=()->{}`;
问题2也是对的,它代表一个无入参但返回字符串的表达式。
问题3 也是正确的,它代表无入参但是返回一个字符串的`lambda`,需要注意的是它加了花括号, 所以要加return 关键字`(花括号内只能有完整的句子,不可缩写)`。
问题4错误,如笔者前1题所说,在没有花括号的情况下,就不可以有return关键字及封号。
问题5也是错误的,存在花括号情况下内部必须是完整的代码句子。
2
3
4
5
# 函数式接口
想要知道哪里可以使用lambda表达式,我们需要先介绍一下函数式接口,因为lambda就是为精简语法的着重表达语义而生。我们以Runnable接口为例,它就是一个典型的函数式接口,它是一个接口,通过FunctionalInterface注明(这个注解非必须的,它仅仅是语义上告知开发这个接口可用lambda表达式简化表达),有且只有一个抽象方法,这一点非常重要,只有唯一的抽象方法才能保证我们的lambda能够正确的被编译器推断从而正常运行:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
2
3
4
这里我们不妨做个简单小测,请问下面哪个接口是函数式接口:
public interface Adder{
int add(int a, int b);
}
public interface SmartAdder extends Adder{
int add(double a, double b);
}
public interface Nothing{
}
2
3
4
5
6
7
8
答案是只有Adder,原因很简单SmartAdder 继承了Adder由此得到了两个抽象方法不具备类型推断的唯一性,而Nothing没有任何抽象方法,就更加不可行了。
# 函数描述符
我们上文编写Comparator接口时提到一个函数签名的概念,这里我们还是以Runnable为例,他的方法是run是一个void类型即无返回值,且它的参数列表为空,由此它的函数描述符就是() -> void,所以我们使用lambda表达式的时候只要按照这个签名编写lambda即可。
再看看一个例子,这也是笔者自定义的一个函数式接口,可以看出他的入参是一个Apple类,返回值是boolean,所以它的函数描述符是(Apple)->boolean
interface ApplePredicate{
//函数签名为 Apple->boolean
boolean test(Apple a);
}
2
3
4
基于此函数描述符,我们就可以按照要求编写一个lambda表达式(a)->true,同样的按照类型推断原则将参数和返回代入,同样编译通过:

# 基于lambda实现环绕执行
我们现有一个需求,要求传入一个文件名给出文件第一行数据。假设我们现在有个文件test.txt,其内容如下:
Java
8
Lambdas
In
Action
2
3
4
5
对应的我们代码示例如下:
public static void main(String[] args) throws IOException {
System.out.println(processFileLimited("F:\\test.txt"));
}
public static String processFileLimited(String fileName) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
return br.readLine();
}
}
2
3
4
5
6
7
8
9
10
11
如果我们希望能够读取两行的拼接结果,读取所有数据,也可能读取一个字符,这时候我们就可以基于函数式接口,将行为抽象为函数式接口,从而实现将用户自定义行为参数化,从而完成个性化功能拓展。
由上述几个需求:
- 读取两行拼接结果。
- 读取文档所有数据。
- 读取一个字符。
这些返回值都要求返回字符串,而我们又需要从文件中读取数据,由此我们的函数式接口的方法可以是入参为BufferedReader 而返回值为String类型:

由此我们将读取文件这个行为封装成一个函数式接口:
@FunctionalInterface
public interface BufferedReaderProcessor {
//函数描述符:BufferedReader->String
String process(BufferedReader b) throws IOException;
}
2
3
4
5
然后我们再将改造对应的方法:
//行为参数将,将BufferedReader实际业务逻辑抽象为函数式接口
public static String processFileLimited(String fileName,BufferedReaderProcessor processor) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
return processor.process(br);
}
}
2
3
4
5
6
后续我们只需按照函数签名给出相应表达式即可:
//读取第一个字符
System.out.println(processFileLimited("F:\\test.txt",(br)->br.readLine().substring(0,1)));
//读取两行
System.out.println(processFileLimited("F:\\test.txt",(br)->br.readLine()+br.readLine()));
2
3
4
# 小结
自此我们通过简单的示例介绍了lambda表达式和函数式编程的理念和使用技巧,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考
《java8 In action》