Java核心技术1-lambda表达式

  |  

摘要: 本文是《Java核心技术 10th》中 lambda 表达式的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap6 中关于【lambda 表达式】的要点总结。

在文章 Java核心技术1-接口 中,我们了解了接口

本文我们介绍 lambda 表达式,这是一种表示可以在将来某个时间点执行的代码块的简洁方法。使用 lambda 表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。


为什么引入 lambda 表达式

我们经常有这样的需求:将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。

例如:要定制比较器完成排序,可以向 sort 方法传入一个 Comparator 对象。

1
2
3
4
5
6
7
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}

Arrays.sort(words, new LengthComparator());

在数组完成排序之前,sort 方法会一直调用 compare 方法,只要元素的顺序不正确就会重新排列元素。如果能将比较元素所需的代码段放在 sort 方法中,这个代码将与其余的排序逻辑集成。

上面的做法是构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

而 lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。


lambda 表达式的写法

常规形式:参数 -> 表达式

1
2
(String first, String second) -> 
first.length() - second.length();

完成的计算无法放在一个表达式中,则需要放在 {} 里并显式给出 return。

1
2
3
4
5
6
7
8
9
(String first, String second) -> {
if(first.length() < second.length()) {
return -1;
} else if (first.length() > second.length()) {
return 1;
} else {
return 0;
}
}

没有参数的情况,也要保留括号。

1
2
3
4
5
() -> {
for (int i = 100; i >= 0; i--) {
System.out.println(i);
}
}

参数类型可以推导的情况,可以忽略其类型。

1
2
Comparator<String> comp = (first, second) -> 
first.length() - second.length();

如果方法只有一个参数,而且这个参数的类型可以推导得出,那么还可以省略小括号。

1
2
3
4
5
6
7
8
ActionListener listener = event -> 
System.out.println("The Time is " + new Date());

(event) ->
System.out.println("The Time is " + new Date());

(ActionEvent event) ->
System.out.println("The Time is " + new Date());

无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得出。例如 (String first, String second) -> first.length() - second.length(); 可以再需要 int 类型结果的上下文中使用。

如果一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,例如 (int x) -> {if (x >= 0) return 1;},这是不合法的。

例子: 在比较器中用 lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Arrays;

public class LambdaTest {
public static void main(String[] args) {
String[] words = new String[] {
"Mercury", "Venuw", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune"
};
System.out.println("原始: ");
System.out.println(Arrays.toString(words));

Arrays.sort(words);
System.out.println("字典序排序: ");
System.out.println(Arrays.toString(words));

Arrays.sort(words, (first, second) -> first.length() - second.length());
System.out.println("长度排序: ");
System.out.println(Arrays.toString(words));
}
}

函数式接口

Java 中有很多封装代码块的接口,例如 Comparator 就是一个。lambda 表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口(functional interface)。

注意:接口有可能重新声明 Object 类的方法,例如 clone 和 toString。这种声明可能会让方法不再是抽象的。在 Java8 中,接口可以声明非抽象方法。

例如 Arrays.sort 的第二个参数需要一个 Comparator 实例,而 Comparator 就是只有一个方法的接口,因此可以提供 lambda 表达式 :

1
Arrays.sort(words, (first, second) -> first.length() - second.length());

Arrays.sort 会接收实现了 Comparator<String> 的某个类的对象。在这个对象上调用 compare 方法会执行这个 lambda 表达式。

在有的程序设计语言中,可以声明函数类型,例如 (String, String) -> int、可以声明函数类型的变量,还可以用变量保存函数表达式。但 Java 中没有函数类型,而是保持接口的概念。

在 Java 对 lambda 表达式能做的操作只有转换为函数式接口,lambda 表达式也不能赋给 Object 类型的变量,因为 Object 不是一个函数式接口。

java.util.function 中定义了很多通用的函数式接口。例如:BiFunction<T, U, R> 描述了参数类型为 T 和 U 且返回类型为 R 的函数。lambda 表达式可以赋给该对象,因为它是函数是接口。

1
2
BiFunction<String, String, Integer> comp;
comp = (first, second) -> first.length() - second.length();

但注意,comp 对于排序是没有帮助的,因为 Arrays.sort 不接收 BiFunction。Java 中类似于 Comparator 的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法。

java.util.function 中有一个 Predicate 接口

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

ArrayList 类有一个 removeIf 方法,它的参数就是一个 Predicate,这个接口专门用来传递 lambda 表达式。例如:

1
list.removeIf(e -> e == null);

上面的代码将从一个数组列表中删除所有 null 值。


方法引用

表达式 System.out::println 就是一个方法引用,它等价于 lambda 表达式 x -> System.out.println(x)

假设要对字符串排序,不考虑字母大小写,用方法引用可以写成下面这样:

1
Arrays.sort(words, String::compareToIgnoreCase);

:: 操作符分隔方法名与对象或类名,主要有三种情况:

1
2
3
object::instanceMethod
Class::staticMethod
Class::instanceMethod

在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。例如:

1
2
System.out::println 等价于 x -> System.out.println(x)
Math::pow 等价于 (x, y) -> Math.pow(x, y)

第 3 种情况,第 1 个参数会成为方法的目标,例如:

1
String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y)

如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。例如,Math.max 方法有两个版本,一个用于整数,另一个用于double 值。选择哪一个版本取决于 Math::max 转换为哪个函数式接口的方法参数

类似于 lambda 表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。

在方法引用中还可以使用 this 参数,例如:

1
this::equal 等同于 x -> this.equals(x)

方法引用也可以使用 super,此时用 this 作为目标,会调用给定方法的超类版本,例如:

1
super::instanceMethod

构造器引用

构造器引用与方法引用类似,只不过方法名为 new。例如:

Person::new 是 Person 构造器的一个引用,具体哪一个构造器取决于上下文。

1
2
3
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

以上代码中 stream、map、collect 等方法是单独的话题,以后再来学习。现在的重点是 map 方法会为各个列表元素调用 Person(String) 构造器。

可以用数组类型建立构造器引用,例如 int[]::new 是一个构造器引用,它有一个参数为数组的长度,等价于 lambda 表达式 x -> new int[x]

Java 有一个限制:无法构造泛型类型 T 的数组,new T[n] 会报错,因为它会改为 new Object[n]。数组构造器可以克服这个限制。

假设我们需要一个 Person 对象数组,Stream 接口有一个 toArray 可以返回 Object 数组:

1
Object[] people = stream.toArray();

如果想要得到 Person 引用数组,而不是 Object 引用数组,这个问题在流库中通过构造器引用解决:把 Person[]::new 传入 toArray 方法。

1
Person[] people = stream.toArray(Person[]::new);

toArray 方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。


lambda 表达式的变量作用域

一个 lambda 表达式有 3 个部分:

  1. 一个代码块。
  2. 参数。
  3. 自由变量的值,即非参数而且不在 lambda 表达式内中定义的变量。

其中:

  • 关于代码块以及自由变量值有一个术语:闭包(closure)。 在 Java 中,lambda 表达式就是闭包。
  • 自由变量就是在 lambda 表达式中访问外围方法或类中的变量。

在 lambda 表达式中要访问外围方法或类中的变量,注意以下几点:

  • lambda 表达式可以捕获外围作用域中变量的值。在 Java 中,要确保所捕获的值是明确定义的。还有一个重要的限制,在 lambda 表达式中,只能引用值不会改变的变量。因为如果在 lambda 表达式中改变变量,并发执行多个动作时就会不安全。
  • lambda 表达式内与嵌套块有相同的作用域,lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
  • lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数。

处理 lambda 表达式

使用 lambda 表达式的重点是延迟执行(deferred execution)。因为如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个 lambda 表达式中。

希望延迟执行,主要有以下几个原因:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如,排序中的比较操作);
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码。

例子: 重复一个动作 n 次

我们定义一个 repeat 函数,有两个参数,一个表示次数,另一个表示动作。一个示例调用如下:

1
repeat(10, () -> System.out.println("Repeat 10!"));

第二个参数要接受 lambda 表达式,需要选择(或者自己提供)一个函数式接口。

常用的函数式接口

上面是 Java 中最重要的函数式接口,这里我们可以用 Runnable 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package repeatlambda;

import java.lang.Runnable;

public class RepeatTest {
public static void main(String[] args) {
Runnable func = () -> System.out.println("Repeat 10!");
repeat(10, func);
}

public static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) {
action.run();
}
}
}

在调用 action.run() 的时候,会执行 lambda 表达式的主体。

在前面的重复动作中,如果我们还想知道动作出现在哪一次迭代中,就要相应地换一个合适的函数式接口:其中包含一个方法,有一个整数参数且返回类型为 void。

处理int值的标准接口如下:

1
2
3
public interface IntConsumer {
void accept(int value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package repeatlambda;

import java.util.function.IntConsumer;

public class RepeatTest {
public static void main(String[] args) {
IntConsumer func = i -> System.out.println(i + ": Repeat 10!");
repeat(10, func);
}

public static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) {
action.accept(i);
}
}
}

下面是基本类型 int、long 和 double 的 34 个可能的规范。最好使用这些特殊化规范来减少自动装箱,例如前面的代码中使用了 IntConsumer 而不是 Consumer<Integer>

p, q 为 int, long, double; P, Q 为 Int, Long, Double

大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如,Predicate.isEqual(a) 等同于 a::equals,不过如果 a 为 null 也能正常工作。已经提供了默认方法 and、or 和 negate 来合并谓词。例如:

1
2
Predicate.isEqual(a).or(Predicate.isEqual(b)) 
等同于 x -> a.equals(x) || b.equals(x)

如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。这样做有两个优点:如果无意中增加了另一个非抽象方法,编译器会产生一个错误消息;另外 javadoc 页里会指出你的接口是一个函数式接口。这个注解只是建议,并不是必须的,根据定义,任何有一个抽象方法的接口都是函数式接口。


Comparator 拾遗

Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于 lambda 表达式或方法引用。

例如静态 comparing 方法取一个“键提取器”函数,它将类型 T 映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。

例如,假设有一个 Person 对象数组,可以如下按名字对这些对象排序:

1
Arrays.sort(people, Comparator.comparing(Person::getName));

可以把比较器与 thenComparing 方法串起来:

1
2
Arrays.sort(people, Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));

可以为 comparing 和 thenComparing 方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:

1
2
Arrays.sort(people, Comparator.comparing(Person::getName, 
(s, t) -> Integer.compare(s.length(), t.length())));

comparing 和 thenComparing 方法都有变体形式,可以避免 int、long 或 double 值的装箱。例如:

1
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

如果键函数可以返回 null,则要用 nullsFirst 和 nullsLast 适配器。

naturalOrder 方法可以为任何实现了 Comparable 的类建立一个比较器。

静态 reverseOrder 方法会提供自然顺序的逆序。要让比较器逆序比较,也可以用 reversed 实例方法,例如:

1
2
naturalOrder().reversed()
等同于 reverseOrder()

Share