Java 8函数式编程
2023-05-25 10:55:49 # Language # Java

内容来自《Java 8函数式编程》

1. Lambda表达式

1.1 第一个Lambda表达式

Swing 是一个与平台无关的 Java 类库,用来编写图形用户界面(GUI)。该类库有一个常见用法:为了响应用户操作,需要注册一个事件监听器。用户一输入,监听器就会执行一些操作

1
2
3
4
5
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});

在这个例子中,我们创建了一个新对象,它实现了 ActionListener 接口。这个接口只有一个方法 actionPerformed。匿名内部类实现了该方法。

但是匿名内部类不够简便,样板代码,可读性很差,我们不想传入对象,只想传入行为。

Java 8 中,上述代码可以写成一个 Lambda 表达式

1
button.addActionListener(event -> System.out.println("button clicked"));
  • event 是参数名,和上面匿名内部类示例中的是同一个参数。-> 将参数和 Lambda 表达式的主体分开,而主体是用户点击按钮时会运行的一些代码

  • Lambda 表达式中无需指定类型,程序依然可以编译。这是因为 javac 根据程序的上下文(addActionListener 方法的签名)在后台推断出了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。

为了增加可读性并迁就我们的习惯,声明参数时也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的类型!

1.2 如何辨别Lambda表达式

几种 Lambda 表达式的变体

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1
Runnable noArguments = () -> System.out.println("Hello World");
// 2
ActionListener oneArgument = event -> System.out.println("button clicked");
// 3
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println("World");
};
// 4
BinaryOperator<Long> add = (x, y) -> x + y;
// 5
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
  1. 使用空括号 () 表示没有参数,实现了 Runnable 接口,该接口也只有一个 run 方法,返回类型为 void
  2. 只包含一个参数,可省略参数的括号
  3. 主体可以是一段代码块,可以用返回或抛出异常来退出,只有一行代码的 Lambda 表达式也可使用大括号
  4. Lambda 表达式也可以表示包含多个参数的方法,这行代码创建了一个函数,用来计算两个数字相加的结果。变量 add 的类型是 BinaryOperator,它不是两个数字的和,而是将两个数字相加的那行代码
  5. 可以显式声明参数类型,需要使用小括号将参数括起来

目标类型是指 Lambda 表达式所在上下文环境的类型。比如,将 Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型。

1.3 引用值,而不是变量

在使用匿名内部类时,当你需要引用它所在方法里的变量,需要将变量声明为 final

1
2
3
4
5
6
final String name = getUserName();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});

Java 8 虽然放松了这一限制,可以引用非 final 变量,但是该变量在既成事实上必须是 final。虽然无需将变量声明为 final,但在 Lambda 表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。

  • 既成事实上的 final 是指只能给该变量赋值一次
  • 换句话说,Lambda 表达式引用的是值,而不是变量。
1
2
String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));

如果你试图给该变量多次赋值,然后在 Lambda 表达式中引用它,编译器就会报错:

local variables referenced from a Lambda expression must be final or effectively final

1
2
3
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));

1.4 函数接口

函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型

例如 Swing 中的 ActionListener, 只有一个抽象方法:actionPerformed,被用来表示行为。该接口也继承自一个不具有任何方法的父接口:EventListener

1
2
3
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent event);
}

接口中单一方法的命名并不重要,只要方法签名和 Lambda 表达式的类型匹配即可。

  • 这里的函数接口接受一个 ActionEvent 类型的参数,返回空(void)

JDK 提供了一组核心函数接口会频繁出现,以下罗列一部分

接口 参数 返回类型 示例
Predicate<T> T boolean 这张唱片已经发行了吗
Consumer<T> T void 输出一个值
Function<T, R> T R 获得 Artist 对象的名字
Supplier<T> None T 工厂方法
UnaryOperator<T> T T 逻辑非(!)
BinaryOperator<T> (T, T) T 求两个数的乘积(*)

1.5 类型推断

Lambda 表达式中的类型推断,实际上是 Java 7 就引入的目标类型推断的扩展,例如 Java 7 中的菱形操作符

1
2
Map<String, Integer> diamondWordCounts = new HashMap<String, Integer>();
Map<String, Integer> diamondWordCounts = new HashMap<>();

如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型

1
2
3
// 需要java8
useHashmap(new HashMap<>());
private void useHashmap(Map<String, String> values);

Java 8 更进一步,可省略 Lambda 表达式中的所有参数类型, javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。

程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型罢了。这就是所谓的类型推断

一些示例

使用 Lambda 表达式检测一个 Integer 是否大于 5。这实际上是一个 Predicate (用来判断真假的函数接口)

1
Predicate<Integer> atLeast5 = x -> x > 5;
  • Predicate 只有一个泛型类型的参数,Integer 用于其中。
  • Lambda 表达式实现了 Predicate 接口,因此它的单一参数被推断为 Integer 类型。
  • javac 还可检查 Lambda 表达式的返回值是不是 boolean,这正是 Predicate 方法的返回类型

Predicate 接口的源码,接受一个对象,返回一个布尔值

1
2
3
public interface Predicate<T> {
boolean test(T t);
}

BinaryOperator:该接口接受两个参数,返回一个值,参数和值的类型均相同。实例中所用的类型是 Long

1
BinaryOperator<Long> addLongs = (x, y) -> x + y;

但若信息不够,类型推断系统也无能为力,例如代码修改为

BinaryOperator add = (x, y) -> x + y;

编译器给出的报错信息如下:

Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.

上面的例子中并没有给出变量 add 的任何泛型信息,给出的正是原始类型的定义。因此,编译器认为参数和返回值都是 java.lang.Object 实例。

1.6 要点回顾

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda 表达式的常见结构:BinaryOperator add = (x, y) → x + y
  • 函数接口指仅具有单个抽象方法的接口,用来表示 Lambda 表达式的类型。
Prev
2023-05-25 10:55:49 # Language # Java