Lambda 表达式

2020-06-13

1. 引言

我希望通过这一篇文章、可以让读者全面了解Lambda表达式、也许不够全面、我在尽力完善它、也希望你能留下宝贵意见、在下方留言。文章有点长、请耐心看完。

2. 描述

  • 可以将 Lambda 表达式理解为简洁的表示可传递匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可抛出的异常列表。
  • Lambda 表达式是实现行为参数化的一种方式,这种方式比起使用匿名内部类的方式更加的简洁、易读。

3. 组成

  • 参数
  • 箭头
  • 主体

例如

1
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())


4. Lambda 语法

  • (parameters) -> expression
  • (parameters) -> { statements; }

5.有效的 Lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 表示有一个 String 类型的入参,且返回一个 int 类型的结果。  */
(String s) -> s.length()

/* 表示有一个 Apple 类型的入参,且返回一个 boolean 类型的结果。 */
(Apple a) -> a.getWeight() > 150

/* 表示有两个 int 类型的入参,且没有返回值(返回 void )。 */
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y);
}

/* 表示没有入参,且返回一个 int 类型的结果。 */
() -> 1

/* 表示有两个 Apple 类型的入参,且返回一个 int 类型的结果,比较两个苹果的重量。 */
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

6. 使用 Lambda 表达式

6.1 函数式接口

  • 定义:只定义了一个抽象方法的接口称为函数式接口。接口中可以包含多个 default 方法,只要接口只定义了一个抽象方法、那么该接口就是函数式接口,比如java.lang.Runnable 类和 java.util.Comparator 类。
  • 作用:Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,且将整个表达式作为函数式接口的实例(具体来说、Lambda表达式是函数式接口的一个实例)。
  • 备注:可以使用 @FunctionalInterface 注解注明接口是函数式接口,提高代码的可读性。若接口包含多个抽象方法(非函数式接口),使用该注解将会编译报错。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {

Runnable r1 = () -> System.out.println(" 使用 Lambda 表达式。 ");

Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println(" 使用匿名类。 ");
}
};

process(r1);
process(r2);

}

private static void process(Runnable r1) {
if (Objects.nonNull(r1)) {
r1.run();
}
}

结论: 函数式接口通过使用 Lambda 表达式的方式创建接口的一个实例,比通过匿名类的方式,代码更加的简洁大方。

6.2 常用函数式接口

Java API中已经有几个函数式接口,如Comparable、Runnable、Callable

6.2.1 Predicate

java.util.function.Predicate 接口是一个函数式接口,只包含一个test()抽象方法,接受泛型 T 对象,返回一个 boolean 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 定义学生类
*
* @author : sungm
* @since : 2019/8/7
*/
public class Student {

private String id;
private String name;
private String sex;
private Integer age;
private Double weight;
private Long height;

//省略了 get/set方法, 默认构造函数, 全构造函数
}


/**
* 主类
*
* @author : sungm
* @since : 2019-08-23 16:24
*/
public class Main {

public static void main(String[] args) {
/* 通过Lambda表达式创建一个Predicate实例(test方法表达式为年龄大于10岁) */
Predicate<Student> agePredicate = (Student student) -> student.getAge() > 10;
/* 通过Lambda表达式创建一个Predicate实例(test方法表达式为身高高于165cm) */
Predicate<Student> heightPredicate = (Student student) -> student.getHeight() > 165L;

/* 创建一些数据 */
Student sungm = new Student("1", "sungm", "1", 25, 69.1D, 178L);
Student sunzm = new Student("2", "sunzm", "1", 22, 60.0D, 176L);
Student sunhw = new Student("3", "sunhw", "1", 1 , 4.1D , 53L);
Student sunll = new Student("4", "sunll", "2", 24, 55.2D, 168L);
List<Student> students = Arrays.asList(sungm, sunzm, sunhw, sunll);

/* 筛选出年龄大于10岁的学生 */
List<Student> ageFilterStudents = students.stream().filter(agePredicate).collect(Collectors.toList());

/* 筛选出年龄小于或等于10岁的学生 */
List<Student> ageNegateFilterStudents = students.stream().filter(agePredicate.negate()).collect(Collectors.toList());

/* 筛选出身高高于165cm的学生 */
List<Student> heightFilterStudents = students.stream().filter(heightPredicate).collect(Collectors.toList());

/* 筛选出 年龄大于10岁 且 身高高于165cm 的学生 */
List<Student> ageAndHeightFilterStudents = students.stream().filter(agePredicate.and(heightPredicate))
.collect(Collectors.toList());

/* 筛选出 年龄大于10岁 或 身高高于165cm 的学生 */
List<Student> ageOrHeightFilterStudents = students.stream().filter(agePredicate.or(heightPredicate))
.collect(Collectors.toList());
}
}

注意: Stream.filter()方法接收一个Predicate入参,可用于Java 8 的 Stream 中,其源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Stream<T> extends BaseStream<T, Stream<T>> {

/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* predicate to apply to each element to determine if it
* should be included
* @return the new stream
*/
Stream<T> filter(Predicate<? super T> predicate);

//其他方法省略
...
}
6.2.2 Consumer

java.util.function.Consumer 接口是一个函数式接口,只包含一个accept()抽象方法,接受泛型 T 对象,没有返回值(void)。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
//造一批假数据、用于测试
List<Student> students = createStudents();

//定义一个Consumer
Consumer<Student> studentConsumer = (Student student) -> student.setOverWeight(student.getWeight() > 50);
for (Student student : students) {
//消费student
studentConsumer.accept(student);
}
}
6.2.3 Function

java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个
泛型T的对象,并返回一个泛型R的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//造一批假数据、用于测试
List<Student> students = Test1.createStudents();

//定义一个function, 其接收一个student对象,返回student的名称
Function<Student, String> function = Student::getName;
List<String> studentNames = new ArrayList<>();
for (Student student : students) {
studentNames.add(function.apply(student));
}

System.out.println(studentNames);
}

注意

任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要 Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。


7. 类型检查、类型推断及限制

7.1 类型检查

Lambda 类型是从使用 Lambda 的上下文推断出来的,上下文中 Lambda 表达式需要的类型成为目标类型。

类型检查示例图
类型检查示例图

7.2 同样的 Lambda , 不同的函数式接口

有了目标类型的概念,同一个 Lambda 表达式就可以与不同的函数式接口联系起来,只要他们的抽象方法能够兼容。

举例

1
2
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;

特殊的void兼容规则

如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当
然需要参数列表也兼容)

举例

1
2
3
4
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

7.3 类型推断

你还可以进一步简化你的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合 Lambda 表达式,这意味着它也可以推断出适合 Lambda 的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解 Lambda 表达式的参数类型,这样就可以在 Lambda 语法中省去标注参数类型。

举例

1
2
3
4
// 没有推断类型
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有推断类型
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

注意

有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,你必须做出自己的选择。

7.4 使用局部变量

我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。

举例

1
2
3
int x = 1;
// Lambda 表达式使用了自由变量 x
IntFunction<Integer> function = y -> x + y;

说明

Lambda 可以无限制的获取实例变量和静态变量,但局部变量必须显示声明为 final 类型、或者事实上是 final 类型。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。)

为什么局部变量有这些限制

  1. 实例变量保存在堆中,局部变量保存在栈中。如果 Lambda 可以直接访问局部变量,则使用 Lambda的线程,它可能会在分配该局部变量的线程收回该局部变量之后访问该局部变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  2. 这一限制不鼓励你使用改变外部变量的典型命令式编程模式

7.5 方法引用

方法引用可以让你重复的使用现有的方法定义,并像 Lambda 一样传递他们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。

举例

1
2
3
4
//使用 Lambda 表达式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
//使用方法引用
inventory.sort(comparing(Apple::getWeight));

7.5.1 普通方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义
public class Apple {

//省略其他属性...
private String weight;

public String getWeight() {
return this.weight;
}

}

//普通方法引用
inventory.sort(comparing(Apple::getWeight));

7.5.1 构造方法引用

1
2
3
4
5
6
7
8
9
10
11
//定义
public class Apple {

//省略其他属性...
//定义默认构造函数
public Apple() {
return this.weight;
}
}
//构造方法引用(下例引用的是默认构造函数)
Supplier<Apple> supplier = Apple::new;

7.5.1 静态方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义
public class Apple {

private static final Apple INSTANCE = new Apple();

private Apple() {
}

public static Apple getInstance() {
return INSTANCE;
}
}

//静态方法引用
Apple::getInstance;