函数式编程与Java 中的 Lambda 表达式
函数式编程 Functional Programming
函数式编程,早在 50 多年前就已经出现,近几年又逐渐引人关注,出现了很多新的函数式编程语言,比如 Clojure、Scala、Erlang 等。
一些非函数式编程语言也加入了很多特性、语法、类库来支持函数式编程,比如 Java、Python、Ruby、JavaScript 等。除此之外,Google Guava 也有对函数式编程的增强功能。
严格上来讲,函数式编程中的「函数」,并不是指我们编程语言中的「函数」概念,而是指数学「函数」或者「表达式」,比如,。不过,在编程实现的时候,对于数学「函数」或「表达式」,我们一般习惯性地将它们设计成函数。所以,如果不深究的话,函数式编程中的数学概念「函数」也可以理解为编程语言中的「函数」。
在科学计算、数据处理、统计分析这些领域,程序往往比较容易用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以用很少的代码就能搞定。
函数式编程的核心特点是,函数作为一段功能代码,可以像变量一样进行引用和传递,以便在有需要的时候进行调用。
函数式编程和面向过程编程的区别
函数式编程的代码实现跟面向过程编程一样,也是以函数作为组织代码的单元。它跟面向过程编程的区别在于,函数式编程的函数是无状态的。函数内部涉及的变量都是局部变量,不会像 站内文章面向对象编程 那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。这实际上就是数学函数或数学表达式的基本要求。
1 | // 有状态函数: 执行结果依赖b的值是多少,即便入参相同,多次执行函数,函数的返回值有可能不同,因为b值有可能不同。 |
Java 中的函数式编程
Google Guava 对函数式编程的使用是很谨慎的,认为过度地使用函数式编程,会导致代码可读性变差,强调不要滥用。
Functional Explained —— Google Guava
Excessive use of Guava’s functional programming idioms can lead to verbose, confusing, unreadable, and inefficient code. These are by far the most easily (and most commonly) abused parts of Guava, and when you go to preposterous lengths to make your code “a one-liner,” the Guava team weeps.
函数类型与 @FunctionalInterface
C 语言支持函数指针,它可以把函数直接当变量来使用。Java 没有函数指针这样的语法,所以它通过函数接口,将函数包裹在接口中当作变量来使用。
Java 对函数式编程的支持,本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口,然后对接口冠以 @FunctionalInterface
注解,那么这个接口就可以作为「函数类型」,可以接收一段以 Lambda 表达式,或者方法引用予以承载的逻辑代码。
被 @FunctionalInterface
标注的接口仅声明一个方法,一旦这个函数定义好,它能执行的功能是确定的,就是调用和不调用的区别。显然,这个方法可以用来代表「函数类型」所能执行的功能,接口中声明的方法就是和函数体定义一一对应的。
@FunctionalInterface
下只能声明一个未实现的方法,多一个、少一个都不能编译通过。但有以下例外:
- 覆写
Object
中toString/equals
的方法不受此个数限制。 default
方法和static
方法因为带有实现体,所以也不受此限制。
@FunctionalInterface
注解不是必须的,不加这个注解的接口(前提是只包含一个方法)一样可以作为函数类型。不过,显而易见的是,加了这个注解表意更明确、更直观,是更被推荐的做法。
JDK 提供的函数类型
java.util.function
包下预定义了常用的函数类型,包括:
1 | // 无参无返回型函数可以使用 Runnable 接口 |
这些个定义,都是在参数个数(0、1、2 个)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口,比如 Predicate
、LongSupplier
等等。
1 |
|
Lambda 表达式
Lambda 表达式写法
遍历集合除了 站内文章基本的方式 以外,还可以使用 Lambda 表达式。Lambda 表达式又称函数式表达,是 Java 中典型的语法糖。
Lambda 表达式语法:
1 | (Type parameter, ... ) ->{ statements; } |
parameters
是参数列表, { statements; }
是 Lambda 表达式的主体。
简化写法:
- 参数简化:
parameters
如果只有一个参数,可以省略小括号()
;如果没有参数,也需要空括号()
。parameters
可以省略参数类型。
- 语句简化:
- 无返回值:
statements;
只包含一条语句,无返回值,可省略大括号{}
、分号;
- 有返回值:
- 一般写法:
(int x)->{return x*x;}
- 普通的表达式:
(int i,int j)->(i*j+2)
- 可省略括号:
x->x*x
- ❌错误写法:
x->return x*x
- 一般写法:
- 无返回值:
Lambda 表达式代替匿名内部类
@FunctionalInterface
声明了函数类型,Lambda 表达式就用来定义函数类型的实现体。在介绍 Lambda 简化原理之前,首先回忆以下关于 Java 接口的知识。
Java 的 interface
接口有以下特点:
- 接口不能被实例化,只能通过它的实现类来实现。
- 不允许创建接口的实例,但允许定义接口类型的引用变量,该变量引用实现了这个接口类的实例。如:
Photographable t=new Camera();
,其中Photographable
为接口类型,Camera
为实现了这个接口的类。 - 可以使用匿名内部类创建接口类型的引用变量。匿名内部类中必须实现接口的所有方法。
1 | // Flyable 为接口类型 |
在没有 Lambda 之前,有些方法要求传入接口类型的参数。我们需要先用匿名内部类创建接口类型的引用变量,再把它传进去。现在 Java 8 中可以通过 Lambda 语法糖快速实现一个单方法的匿名内部类。
更详细地说,Lambda 表达式只能赋值给声明为函数式接口的 Java 类型的变量(注解 @FunctionalInterface
)。下文例子中的 Runnable
、Consumer
、Comparator
都是被 @FunctionalInterface
标注的函数式接口,因此可以接受 Lambda 表达式。
例子 1:线程的创建
例子 2:foreach
Java 集合都实现了 java.util.Iterable
接口。forEach()
方法有一个 Consumer
接口类型的 action
参数,它包含了对集合中每个元素的具体操作行为。action
参数所引用的 Consumer
实例必须实现 Consumer
接口的 accept(T t)
方法,在该方法中指定对参数 t
所执行的具体操作。
1 | // Java Iterable 接口源代码 |
示例:
1 | public static void main(String[] args) { |
以上 Lambda 表达式相当于创建了一个 Consumer
类型的匿名对象,并实现了 Consumer
接口的 accept(T t)
方法,此处传给 accept(T t)
方法的参数为 Person
对象。在 Lambda 表达式中符号 ->
后的可执行语句块相当于 accept(T t)
方法的方法体。
例子 3:排序
详看文章:站内文章Java 中的排序。
例 4:与 Stream API 连用
Collection
接口的 stream()
方法返回一个 Stream
对象,程序可以通过这个 Stream
对象操纵集合中的元素。
Lambda 表达式可操纵的变量作用域
this
关键字实际上引用的是外部类的实例。
在 Lambda 表达式中访问的局部变量必须符合以下两个条件之一:
- 条件一:最终局部变量,即用
final
修饰的局部变量。 - 条件二:实际上的最终局部变量,即虽然没有被
final
修饰,但在程序中不会改变局部变量的值。
方法引用
在编译器能根据上下文来推断 Lambda 表达式的参数的场合,可以在 Lambda 表达式中省略参数,直接通过 ::
符号来引用方法。
方法引用就是对一个类中已经存在的方法加以引用,分以下类型:
- 引用类的静态方法
- 对类构造方法的引用,如
ClassName::new
。 - 对类静态方法的引用,如
ClassName::staticMethodName
。编译器会根据上下文推断到底用哪个对象的实例方法。
- 对类构造方法的引用,如
- 引用实例方法
objectName::instanceMethod
,或者new Test()::instanceMethod
ClassName::instanceMethod
。编译器会根据上下文推断到底用哪个对象的实例方法。
示例:
1 | x -> new BigDecimal(x) // 等同于 BigDecimal::new |