Java: Lambda Expressions and Functional Interfaces
Functional Interfaces
A functional interface is a interface with a single abstract method.
之前提过的 Comparable 接口就只有一个 compareTo 方法,所以它就是一个 functional interface。
Comparator 接口虽然有很多方法,但只有 compare 方法没有默认实现,所以它也是一个 functional interface。
创建包 lambdas,类 LambdasDemo 以及接口 Printer。
1 | public interface Printer { |
作为一个接口,Printer 可能有不同的实现。
1 | public class ConsolePrinter implements Printer{ |
LambdasDemo 的代码如下:
1 | public class LambdasDemo { |
运行后可以看到“Hello World”信息。
有时我们不想显式地创造一个类去实现某个接口,因为这需要写很多代码,而这个类我们只想用一次。
下一节将介绍匿名内部类来解决上述问题。
Anonymous Inner Classes
我们可以将 LambdasDemo 的代码更改如下:
1 | public class LambdasDemo { |
在 new 后我们不再创建一个 ConsolePrinter 实例,而是输入想要利用的接口的名称,然后按 enter 键,编译器就会为我们创建一个匿名内部类。
匿名是指这个类没有名字,它只是一个实现。
内部是指这个类包含在一个方法里。
匿名内部类允许我们通过使用更少的代码来达到同样的效果。
但 Java 中还有另一种方法更简洁,那就是 lambda 表达式。
Lambda Expressions
lambda 表达式在代码中的形式就是 ->
。
利用 lambda 表达式,上一节的代码可以按如下方式重写:
1 | public class LambdasDemo { |
上述代码还可以更简洁。
首先我们可以删除 message 的类型名 String。编译器会知道 message 是 String 类型的,因为 lambda 表达式所表达的内容应该是 Printer 接口的一个匿名实现,而 Printer 接口中的 print 方法是以 String 类型的参数为输入的,所以 message 也应该是 String 类型的。
如果只有一个参数要传入,我们还可以去掉对应的括号。我们在无参数或者有多个参数的情况下需要使用括号,多个参数之间要用逗号隔开。
如果函数体只有一行代码,我们还可以去掉对应的花括号。在花括号边上按下 option + enter,选择 Replace with expression lambda 即可以去掉花括号。
1 | public class LambdasDemo { |
我们还可以把 lambda 表达式存储在一个变量中:
1 | public class LambdasDemo { |
Variable Capture
在 lambda 表达式中,我们使用了我们在表达式内声明的变量 message,但我们也可以使用在封闭方法中声明的局部变量:
1 | public class LambdasDemo { |
上面代码中 prefix 就是我们在封闭方法 show 中声明的变量。
我们也可以访问封闭类中的静态方法或静态变量:
1 | public class LambdasDemo { |
上面代码中 prefix 是在封闭类 LambdasDemo 中声明的静态变量。
我们还可以访问实例字段,例如可以把上面代码中 show 方法和变量 prefix 的 static 声明全部去除,代码仍是可以运行的。
在 lambda 表达式中,还可以使用 this 关键字来引用表达式所在的方法所在的类。如上面的代码中,lambda 表达式中如果有 this 关键字,它就表示的是类 LambdasDemo 在当时语境下创造的对象。
但在匿名内部类中,this 引用的是这个匿名内部类。
这两种情况下的另一个不同点是,匿名内部类可以有状态,它可以用一些字段来存储一些数据。而在 lambda 表达式中,我们不能有字段。
Method References
有时我们在 lambda 表达式中所做的一切不过就是把参数传入一个已经存在的方法中去,就像上面我们就是把 message 传入到 sout 中去。在这种情况下,直接使用方法引用可能更简单。
1 | public class LambdasDemo { |
方法引用的格式如上所示,两个 greet 得到的结果相同。我们可以在第一个 greet 后面的 println 方法上按 option + enter,然后选择 Replace lambda with method reference 即可。
通过使用方法引用,我们可以引用一个类中的静态或实例方法,以及构造函数。
引用静态方法:
1 | public class LambdasDemo { |
引用实例方法:
1 | public class LambdasDemo { |
引用构造函数:
1 | public class LambdasDemo { |
Built-in Functional Interfaces
Java 提供了许多可以用来执行普通任务的预定义功能接口,这些接口被定义在了 java.util.function 包中。
功能接口大致可以被分为四类:
- Consumer:这类接口需要且只需要一个参数,并且没有返回结果。它消耗了一个参数,因此被称为消费者。我们之前的 Printer 接口就是个例子。
- Supplier:这类接口与消费者相反,它不接受输入,但会返回一个值。
- Function:函数接口可以将一个值映射到另一个值。
- Predicate:这类接口需要一个输入,并检查这个输入是否满足某些要求。我们可以用它来过滤数据。
The Consumer Interface
Consumer 接口需要一个输入,但没有输出。
它有两个方法 accept 和 andThen,其中 andThen 有默认实现。
这个接口有一些变体,如:
- BiConsumer:它需要两个输入
- IntConsumer:它只接受 int 类型的输入,这可以免去自动封装的过程。类似地,我们还有 LongConsumer 和 DoubleConsumer
接下来介绍如何使用 Consumer 接口。
1 | public class LambdasDemo { |
上述代码中,利用 for 循环或利用 forEach 方法都可以实现对列表的遍历。
其中 forEach 方法期待的是一个 Consumer 对象作为输入。
由于 Consumer 接口是一个功能性接口,所以我们可以用 lambda 表达式来表示。
凡是使用 for,if/else,switch/case 语句的编程都属于命令式编程。
不用命令来指示需要做的事情(how something should be done),而只是指明需要做的事是什么(what should be done),这种编程属于声明式编程。
Chaining Consumers
Consumer 接口有一个默认实现的方法叫做 andThen,通过这个方法,我们可以把 consumers 链接起来,也就是说我们可以按顺序来执行许多操作。
下面的代码中我们会声明一个 Consumer
1 | public class LambdasDemo { |
上面的代码我们运用 andThen 方法,把 print 和 printUpperCase 链接起来,得到的输出结果是:
a
A
b
B
c
C
将光标放到 andThen 上面,然后在上方工具栏打开 Navigate 菜单,选择 Declaration,快捷键为 command + B,然后这就可以看到这个方法的源码。
1 | default Consumer<T> andThen(Consumer<? super T> after) { |
这个方法接受一个消费者 after 作为输入,首先它确认这个 after 不是 null,然后依次调用当前消费者的 accept 方法和 after 的 accept 方法。每次调用这个方法,都会得到一个新的消费者,这意味着我们可以在此调用 andThen 方法。
1 | public class LambdasDemo { |
The Supplier Interface
Supplier 接口与 Consumer 接口相反,它不消耗参数,而是提供一个参数。
Supplier 接口只有一个抽象方法 get。
1 | public class LambdasDemo { |
由于上面的代码只是返回了一个值,所以可以去掉花括号和 return 关键词:
1 | public class LambdasDemo { |
上述代码需要注意的是,Math.random 函数在我们明确地调用它之前并没有被执行,这被叫做 lazy evaluation。直到我们明确提出要求之前,值不会被产出。
类似 Consumer 接口,Supplier 接口也有一些变体比如说 DoubleSupplier,IntSupplier,LongSupplier 以及 BooleanSupplier。通过使用这些 primitive 专业化的接口,可以免去自动封装和自动解封装的消耗。
The Function Interface
顾名思义,这类接口表示的是一个方程,或者是一个带函数的操作,然后返回一个值。因此接口定义时需要两个 generic 类型的参数,一个用来定义输入参数的类型,另一个表示输出参数的类型。
这个接口中有四个方法,其中只有 apply 方法没有实现。
它的变体有 BiFunction,接受两个参数作为输入,返回一个参数作为输出。
它的变体也有 primitive 专业化的接口,这些接口可以分为三类:
- 输入参数有特定的类型,输出参数无特定的类型。比如说 IntFunction 会接受一个 int 类型的参数作为输入,但输出的参数类型在声明接口时定义。同理还有 LongFunction 和 DoubleFunction。
- 输出参数有特定的类型,输入参数无特定的类型。比如说 ToIntFunction 会输出一个 int 类型的参数,但输入的类型在声明接口时定义。
- 输入与输出都有特定的类型。如 IntToLongFuction 会接受一个 int 类型的输入,返回一个 long 类型的输出。
1 | public class LambdasDemo { |
Composing Functions
这一节讨论如何通过组合小的函数来得到更复杂有趣的函数。
1 | public class LambdasDemo { |
当进行 Declarative Programming 的时候,代码的拍不应该按上面的代码所示,这样可以更清晰。
之前说过,把光标放到想要了解的函数名上,然后在上方工具栏中的 Navigate 菜单中找到 Declaration,快捷键 command + B,就可以查看函数的源码。
The Predicate Interface
Predicate 接口用于过滤数据。
这个接口唯一的抽象方法是 test,给他一个参数,可以返回一个布尔值。
它的专业化有 BiPredicate,接受两个参数,返回一个布尔值。
它的 primitive 专业化有 IntPredicate,LongPredicate,DoublePredicate 等。
1 | public class LambdasDemo { |
Combining Predicates
与 Function 接口类似,Predicate 接口也可以通过组合实现更复杂更有趣的操作。
1 | public class LambdasDemo { |
与逻辑操作的与或非类似,Predicate 接口也有 and,or 和 negate 方法分别表示与或非。
The BinaryOperator Interface
之前说过,java.util.function 包中的接口都可以分为 Consumer,Supplier,Function 和 Predicate 四类接口。
但还有一种特殊的接口叫做 BinaryOperator。
这个接口接收两个 T 类型的参数作为输入,然后返回一个 T 类型的输出。
它也有 primitive 专业化,比如 IntBinaryOperator,两个输入和一个输出都是 int 类型的。
1 | public class LambdasDemo { |
上面的代码使用 IntBinaryOperator 会更高效,尤其是在处理较大数据的时候。
1 | public class LambdasDemo { |
The UniaryOperator Interface
这个接口继承了 Function
1 | public class LambdasDemo { |
Summary
- Lambda expressions
- Functional interfaces
- Consumers
- Suppliers
- Functions
- Predicates
- Composition