函数式编程与 Lambda
# 函数式编程与 Lambda
本文主要参考廖雪峰老师的教程函数式编程 (opens new window),并自己动手实践了一遍。
# 函数式编程
我们先看看什么是函数。函数是一种最基本的任务,一个大型程序就是一个顶层函数调用若干底层函数,这些被调用的函数又可以调用其他函数,即大任务被一层层拆解并执行。所以函数就是面向过程的程序设计的基本单元。
Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带 this
参数的函数。
而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU 执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如 C 语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如 Lisp 语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
函数式编程最早是数学家阿隆佐·邱奇 (opens new window)研究的一套函数变换逻辑,又称 Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为 Lambda 计算。
Java 平台从 Java 8 开始,支持函数式编程。
函数式编程是一个比较复杂的学科,本系列课程暂时不会深入去讲解,先简单了解下 Java 里关于函数式编程的新特性
# Java 的方法
在了解 Lambda 之前,我们先回顾一下 Java 的方法。
Java 的方法分为实例方法以及静态方法,例如 Integer
定义的 equals()
方法和 parseInt()
方法:
public final class Integer {
boolean equals(Object o) {
...
}
public static int parseInt(String s) {
...
}
}
2
3
4
5
6
7
8
9
无论是实例方法,还是静态方法,本质上都相当于过程式语言的函数,只不过 Java 的实例方法隐含地传入了一个 this
变量,即实例方法总是有一个隐含参数 this
。
函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是 Lambda 演算,所以我们经常把支持函数式编程的编码风格称为 Lambda 表达式。
# Lambda 表达式
在 Java 程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
- Comparator
- Runnable
- Callable
以 Comparator
为例,我们想要调用 Arrays.sort()
时,可以传入一个 Comparator
实例,以匿名类方式编写如下:
String[] array = {"peter", "jxl", "hello"};
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2){
return s1.compareTo(s2);
}
});
2
3
4
5
6
上述写法非常繁琐。
从 Java 8 开始,我们可以用 Lambda 表达式替换单方法接口。改写上述代码如下:
String[] array = new String[] { "peter", "jxl", "hello"};
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
2
3
4
观察 Lambda 表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
2
3
其中,参数是 (s1, s2)
,参数类型可以省略,因为编译器可以自动推断出 String
类型。-> { ... }
表示方法体,所有代码写在内部即可。Lambda 表达式没有 class
定义,因此写法非常简洁。
如果只有一行 return xxx
的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的类型也是由编译器自动推断的,这里推断出的返回值是 int
,因此,只要返回 int
,编译器就不会报错。
# FunctionalInterface
我们把只定义了单方法的接口称之为 FunctionalInterface
,用注解 @FunctionalInterface
标记。例如,Callable
接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
2
3
4
再来看 Comparator
接口的源码:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
虽然 Comparator
接口有很多方法,但只有一个抽象方法 int compare(T o1, T o2)
,其他的方法都是 default
方法或 static
方法。另外注意到 boolean equals(Object obj)
是 Object
定义的方法,不算在接口方法内。因此,Comparator
也是一个 FunctionalInterface
。
# 方法引用
使用 Lambda 表达式,我们就可以不必编写 FunctionalInterface
接口的实现类,从而简化代码:
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
2
3
实际上,除了 Lambda 表达式,我们还可以直接传入方法引用。例如:
import java.util.Arrays;
public class LearnLambda2 {
public static void main(String[] args) {
String[] array = {"Apple", "Orange", "Banana", "Lemon"};
Arrays.sort(array, LearnLambda2::cmp); //Apple, Banana, Lemon, Orange
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2){
return s1.compareTo(s2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意第 6 行,Arrays.sort()
中直接传入了静态方法 cmp
的引用,用 LearnLambda2::cmp
表示。
因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。
因为 Comparator<String>
接口定义的方法是 int compare(String, String)
,和静态方法 int cmp(String, String)
相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为 Lambda 表达式传入
注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
# 引用实例方法
上一小节,我们引用的是静态方法,可不可以改成实例方法呢?我们来测试下,可以将上一小节的代码改成:
Arrays.sort(array, String::compareTo);
不但可以编译通过,而且运行结果也是一样的,这说明 String.compareTo()
方法也符合 Lambda 定义。
观察 String.compareTo()
的方法定义:
public final class String {
public int compareTo(String o) {
...
}
}
2
3
4
5
这个方法的签名只有一个参数,为什么和 int Comparator<String>.compare(String, String)
能匹配呢?
因为实例方法有一个隐含的 this
参数,String
类的 compareTo()
方法在实际调用的时候,第一个隐含参数总是传入 this
,相当于静态方法:
public static int compareTo(this, String o);
所以,String.compareTo()
方法也可作为方法引用传入。
# 引用构造方法
除了可以引用静态方法和实例方法,我们还可以引用构造方法。
我们来看一个例子:如果要把一个 List<String>
转换为 List<Person>
,应该怎么办?
class Person{
String name;
public Person(String name){
this.name = name;
}
}
List<String> names = List.of("peter", "JXL", "Hello");
List<Person> persons = new ArrayList<>();
2
3
4
5
6
7
8
9
传统的做法是先定义一个 ArrayList<Person>
,然后用 for
循环填充这个 List
:
List<String> names = List.of("peter", "JXL", "Hello");
List<Person> persons = new ArrayList<>();
for (String name : names) {
persons.add(new Person(name));
}
2
3
4
5
但有一个更简单的做法,引用 Person 的构造方法:
List<Person> persons2 = names.stream().map(Person::new).collect(Collectors.toList());
后面我们会讲到 Stream
的 map()
方法,这里有个印象就行。现在我们看到,这里的 map()
需要传入的 FunctionalInterface 的定义是:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
2
3
4
把泛型对应上,就是方法签名 Person apply(String)
,即传入参数 String
,返回类型 Person
。而 Person
类的构造方法恰好满足这个条件,因为构造方法的参数是 String
,而构造方法虽然没有 return
语句,但它会隐式地返回 this
实例,类型就是 Person
,因此,此处可以引用构造方法。构造方法的引用写法是 类名::new
,因此,此处传入 Person::new
。
# 参考
Lambda 的基本概念:Lambda 基础 - 廖雪峰的官方网站 (opens new window)
Lambda 在工作中的使用:公司新来了一个同事,把 Lambda 表达式运用的炉火纯青! (opens new window)
- 01
- 中国网络防火长城简史 转载10-12
- 03
- 公告:博客近期 RSS 相关问题10-02