Java8函数式编程Lambda表达式

  1. 1. Lambda表达式基本介绍
    1. 1.1 简介
    2. 1.2 通俗理解
    3. 1.3 作用
    4. 1.4 语法规则
  2. 2. Lambda表达式的使用
    1. 2.1 实现Runnable
    2. 2.2 遍历列表
    3. 2.3 函数式接口Predicate
    4. 2.4 Map 和 Reduce
      1. 2.4.1 Map
      2. 2.4.2 Reduce
    5. 2.5 过滤创建一个 String 列表
    6. 2.6 对列表的每个元素应用函数
    7. 2.7 复制不同的值,创建一个子列表
    8. 2.8 计算集合元素的统计值
  3. 3. Stream对象的使用
    1. 3.1 Stream对象的中间操作
    2. 3.2 Stream对象的终端操作
  4. 4. 参考资料

1. Lambda表达式基本介绍

1.1 简介

Lambda表达式是一个匿名函数,Lambda表达式基于数学中的λ得名。编程中提到的Lambda表达式,通常是指在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。

如果接口只有一个方法,这时使用匿名内部类时,代码看上去既不清晰也不简洁。在这种情况下,你会希望将函数作为参数传递进去,来让代码看起来更加简洁。Lambda表达式可以实现这种需求,将函数作为方法参数或者将代码作为数据,可以使单方法接口更加简洁。

Java8 中的 Lambda 表达式通常使用 (arguments) -> (expression) 语法书写。以下是 Lambda 表达式的重要特征:

  • 可选参数:Lambda 表达式可以有零个或多个参数。
  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

Lambda表达式是不是语法糖的说明:

  • Labmda表达式不是匿名内部类的语法糖,但是它也是一个语法糖,实现方式其实是依赖了几个JVM底层提供的lambda相关API。
  • 如果是匿名内部类的语法糖,那么编译之后会有两个class文件,但是包含Labmda表达式的类编译后只有一个文件。

1.2 通俗理解

我们知道,对于一个Java变量,我们可以赋给其一个“值”。

Lambda通俗理解-1

如果你想把“一块代码*赋给一个Java变量,应该怎么做呢?

比如,我想把右边那块代码,赋给一个叫做aBlockOfCode的Java变量:

Lambda通俗理解-2

在Java 8之前,这个是做不到的。但是Java 8问世之后,利用Lambda特性,就可以做到了。

Lambda通俗理解-3

当然,这个并不是一个很简洁的写法。所以,为了使这个赋值操作更加elegant, 我们可以移除一些没用的声明。

Lambda通俗理解-4

这样,我们就成功的非常优雅的把“一块代码”赋给了一个变量。而“这块代码”,或者说“这个被赋给一个变量的函数”,就是一个Lambda表达式。

但是这里仍然有一个问题,就是变量aBlockOfCode的类型应该是什么?

在Java 8里面,所有的Lambda的类型都是一个接口,而Lambda表达式本身,也就是”那段代码“,需要是这个接口的实现。这是理解Lambda的一个关键所在,简而言之就是,Lambda表达式本身就是一个接口的实现。直接这样说可能还是有点让人困扰,我们继续看看例子。我们给上面的aBlockOfCode加上一个类型:

Lambda通俗理解-5

这种只有一个接口函数需要被实现的接口类型,我们叫它“函数式接口”。为了避免后来的人在这个接口中增加接口函数导致其有多个接口函数需要被实现,变成”非函数接口”,我们可以在这个上面加上一个声明@FunctionalInterface, 这样别人就无法在里面添加新的接口函数了:

Lambda通俗理解-6

这样,我们就得到了一个完整的Lambda表达式声明:

Lambda通俗理解-7

1.3 作用

最直观的作用就是使得代码变得异常简洁。我们可以对比一下Lambda表达式和传统的Java对同一个接口的实现:

Lambda表达式的作用

这两种写法本质上是等价的。但是显然,Java 8中的写法更加优雅简洁。并且,由于Lambda可以直接赋值给一个变量,我们就可以直接把Lambda作为参数传给函数,而传统的Java必须有明确的接口实现的定义,初始化才行。

1.4 语法规则

Lambda 表达式在 Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->”,该操作符被称 为 Lambda 操作符或箭头操作符。

Lambda表达式语法规则

它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的所有参数
  • 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能。

Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”。

1)语法格式一:无参,无返回值,Lambda 体只需一条语句。示例:

1
Runnable r1 = () -> System.out.println("Hello Lambda!");

2)语法格式二:Lambda 需要一个参数。示例:

1
Consumer<String> con =(x)-> System.out.println(x);

Lambda 只需要一个参数时,参数的小括号可以省略。示例:

1
Consumer<String> con = x -> System.out.println(x);

3)语法格式三:Lambda 需要两个参数,并且有返回值。示例:

1
2
3
4
Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};

当 Lambda 体只有一条语句时,return 与大括号可以省略。示例:

1
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

2. Lambda表达式的使用

使用Lambda表达式创建线程的时候,并不需要关心接口名,方法名,参数名。只需要关注它的参数类型,参数个数,返回值即可。

2.1 实现Runnable

开始使用Java 8时,首先做的就是使用Lambda表达式替换匿名类,而实现Runnable接口是匿名类的最好示例。看一下Java 8之前的Runnable实现方法,需要4行代码,而使用lambda表达式只需要一行代码,用() -> {}代码块替代了整个匿名类。

1
2
3
4
5
6
7
8
9
10
// Java 8之前:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();

// Java 8方式:
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();

2.2 遍历列表

列表现在有了一个 forEach() 方法,它可以迭代所有对象,并将你的Lambda代码应用在其中。

1
2
3
4
5
6
7
8
9
// Java 8之前:
List<String> features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
System.out.println(feature);
}

// Java 8之后:
List<String> features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
features.forEach(System.out::println); // 即相当于 features.forEach(n -> System.out.println(n));

使用 Java 8 的方法引用更方便,方法引用由::双冒号操作符标示,双冒号(::)操作符是 Java 中的方法引用。当们使用一个方法的引用时,目标引用放在 :: 之前,目标引用提供的方法名称放在 :: 之后,即目标引用::方法。

2.3 函数式接口Predicate

除了在语言层面支持函数式编程风格,Java 8也添加了一个包,叫做 java.util.function。它包含了很多类,用来支持Java的函数式编程。其中一个便是Predicate,使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向 API 方法添加逻辑,用更少的代码支持更多的动态行为。下面是Java 8 Predicate 的例子,展示了过滤集合数据的多种常用方法,Predicate接口非常适用于做过滤。

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
// Java 8之前
public static void filter(List<String> names, Predicate condition) {
for(String name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}

// Java 8之后
public static void filter(List<String> names, Predicate condition) {
names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {
System.out.println(name + " ");
});
}

public static void main(String[] args){
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

System.out.println("=== Languages which starts with J :");
filter(languages, (str)->str.toString().startsWith("J"));

System.out.println("=== Languages which ends with a ");
filter(languages, (str)->str.toString().endsWith("a"));

System.out.println("=== Print all languages :");
filter(languages, (str)->true);

System.out.println("=== Print no language : ");
filter(languages, (str)->false);

System.out.println("=== Print language whose length greater than 4:");
filter(languages, (str)->str.toString().length() > 4);
}

2.4 Map 和 Reduce

2.4.1 Map

本例介绍最广为人知的函数式编程概念Map,它允许你将对象进行转换。例如在本例中,我们将 costBeforeTax 列表的每个元素转换成为税后的值。将Lambda表达式传到 map() 方法,后者将其应用到流中的每一个元素,然后用 forEach() 将列表元素打印出来。使用流API的收集器类,可以得到所有含税的开销。有 toList() 这样的方法将 map 或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。你甚至可以用流API的 reduce() 方法将所有数字合成一个,下一个例子将会讲到。

1
2
3
4
5
6
7
8
9
10
// 不使用lambda表达式 为每个订单加上12%的税
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for (Integer cost : costBeforeTax) {
double price = cost + .12*cost;
System.out.println(price);
}

// 使用lambda表达式 为每个订单加上12%的税
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
costBeforeTax.stream().map((cost) -> cost + .12*cost).forEach(System.out::println);

2.4.2 Reduce

在上个例子中,可以看到map将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它,在SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduceh() 函数可以接受Lambda表达式,并对所有值进行合并。IntStream这样的类有类似 average()、count()、sum()的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个Java 8的Map Reduce示例里,我们首先对所有价格应用 12% 的VAT,然后用 reduce() 方法计算总和。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不使用lambda表达式 为每个订单加上12%的税并求和
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = 0;
for (Integer cost : costBeforeTax) {
double price = cost + .12*cost;
total = total + price;
}
System.out.println("Total : " + total);

// 使用lambda表达式 为每个订单加上12%的税
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost) -> sum + cost).get();
System.out.println("Total : " + bill);

2.5 过滤创建一个 String 列表

过滤是 Java 开发者在大规模集合上的一个常用操作,而现在使用 Lambda 表达式和流 API 过滤大规模数据集合是非常的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个 lambda 表达式作为过滤逻辑。下面的例子是用 Lambda 表达式过滤 Java 集合。

1
2
3
4
// 使用lambda表达式 过滤字符串列表中每个字符串长度大于2的元素
List<String> strList = Arrays.asList("abc", "bcd", "defg", "jk");
List<String> filtered = strList.stream().filter(x -> x.length()> 2).collect(Collectors.toList());
System.out.printf("Original List : %s, filtered list : %s %n", strList, filtered);

2.6 对列表的每个元素应用函数

如果需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了,如下所示。

1
2
3
4
// 使用lambda表达式 将字符串换成大写并用逗号链接起来
List<String> list = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada");
String g7Countries = list.stream().map(String::toUpperCase).collect(Collectors.joining(", "));
System.out.println(g7Countries);

2.7 复制不同的值,创建一个子列表

本例展示了如何利用流的 distinct() 方法来对集合进行去重。

1
2
3
4
// 使用lambda表达式 用所有不同的数字创建一个平方列表
List<Integer> numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List<Integer> distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
System.out.printf("Original List : %s, Square Without duplicates : %s %n", numbers, distinct);

2.8 计算集合元素的统计值

IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistics,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值,它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

1
2
3
4
5
6
7
// 使用lambda表达式 获取数字的个数、最小值、最大值、总和以及平均值
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : " + stats.getMax());
System.out.println("Lowest prime number in List : " + stats.getMin());
System.out.println("Sum of all prime numbers : " + stats.getSum());
System.out.println("Average of all prime numbers : " + stats.getAverage());

3. Stream对象的使用

Stream对象提供多个非常有用的方法,这些方法可以分成两类:

  • 中间操作:将原始的Stream转换成另外一个Stream;如filter返回的是过滤后的Stream。
  • 终端操作:产生的是一个结果或者其它的复合操作;如count或者forEach操作。

3.1 Stream对象的中间操作

方法 说明
sequential 返回一个相等的串行的Stream对象,如果原Stream对象己经是串行就可能会返回原对象
parallel 返回一个相等的并行的Stream对象,如果原Stream对象己经是并行的就会返回原对象
unordered 返回一个不关心顺序的Stream对象,如果原对象己经是这类型的对象就会返回原对象
onClose 返回一个相等的Steam对象,同时新的Stream对象在执行Close方法时会调用传入的Runnable对象
close 关闭Stream对象
filter 元素过滤:对Stream对象按指定的Predicate进行过滤,返回的Stream对象中仅包含未被过滤的元素
map 元素一对一转换:使用传入的Function对象对Stream中的所有元素进行处理,返回的Stream对象中的元素为原元素处理后的结果
mapToInt 元素一对一转换:将原Stream中的使用传入的IntFunction加工后返回一个IntStream对象
flatMap 元素一对多转换:对原Stream中的所有元素进行操作,每个元素会有一个或者多个结果,然后将返回的所有元素组合成一个统一的Stream并返回;
distinct 去重:返回一个去重后的Stream对象
sorted 排序:返回排序后的Stream对象
peek 使用传入的Consumer对象对所有元素进行消费后,返回一个新的包含所有原来元素的Stream对象
limit 获取有限个元素组成新的Stream对象返回
skip 抛弃前指定个元素后使用剩下的元素组成新的Stream返回
takeWhile 如果Stream是有序的,那么返回最长命中序列(符合传入的Predicate的最长命中序列)组成的Stream;如果是无序的,那么返回的是所有符合传入的Predicate的元素序列组成的Stream
dropWhile 与takeWhile相反,如果是有序的,返回除最长命中序列外的所有元素组成的Stream;如果是无序的,返回所有未命中的元素组成的Stream

3.2 Stream对象的终端操作

方法 说明
iterator 返回Stream中所有对象的迭代器
spliterator 返回对所有对象进行的spliterator对象
forEach 对所有元素进行迭代处理,无返回值
forEachOrdered 按Stream的Encounter所决定的序列进行迭代处理,无返回值
toArray 返回所有元素的数组
reduce 使用一个初始化的值,与Stream中的元素一一做传入的二合运算后返回最终的值。每与一个元素做运算后的结果,再与下一个元素做运算,它不保证会按序列执行整个过程
collect 根据传入参数做相关汇聚计算
min 返回所有元素中最小值的Optional对象;如果Stream中无任何元素,那么返回的Optional对象为Empty
max 返回所有元素中最大值的Optional对象;如果Stream中无任何元素,那么返回的Optional对象为Empty
count 所有元素个数
anyMatch 只要其中有一个元素满足传入的Predicate时返回True,否则返回False
allMatch 所有元素均满足传入的Predicate时返回True,否则False
noneMatch 所有元素均不满足传入的Predicate时返回True,否则False
findFirst 返回第一个元素的Optioanl对象,如果无元素返回的是空的Optional;如果Stream是无序的,那么任何元素都可能被返回
findAny 返回任意一个元素的Optional对象,如果无元素返回的是空的Optioanl
isParallel 判断是否当前Stream对象是并行的

4. 参考资料

[1] Lambda 表达式有何用处?如何使用?from 知乎

[2] Java 8 - 函数编程(lambda表达式) from 全栈技术体系

[3] Java 8 Lambda表达式介绍(一) from cnblogs

[4] Java 8 Lambda表达式介绍(二) from cnblogs

[5] Java 8 Lambda 表达式简介 from DEV

[6] Java 8 Lambda表达式快速入门 from GIthub

[7] Java SE 8:Lambda 快速入门 from 官方文档

[8] Java 8:一文掌握 Lambda 表达式 from 知乎

[9] Lambda 表达式使用例子 from CSDN