一、Lambda 表达式
1、为什么使用 Lambda 表达式
Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
2、函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法—— 强调做什么,而不是以什么形式做。
面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法完成事情。
函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。
3、冗余的 Runnable 代码
传统写法
当需要启动一个线程去完成任务时,通常会通过 java.lang.Runnable 接口来定义任务内容,并使用 java.lang.Thread 类来启动线程。
Demo:
1 public class Demo01Runnable {
2 public static void main(String[] args) {
3 // 匿名内部类
4 Runnable task = new Runnable() {
5 @Override
6 public void run() { // 覆盖重写抽象方法
7 System.out.println("多线程任务执行!");
8 }
9 };
10 new Thread(task).start(); // 启动线程
11 }
12 }
面向对象的思想:首先创建一个 Runnable 接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析
对于 Runnable 的匿名内部类用法:可以分析出几点内容:
① Thread 类需要 Runnable 接口作为参数,其中的抽象方法 run 方法是用来指定线程任务内容的核心
② 为了指定 run 的方法体,必须需要 Runnable 接口的实现类;
③ 为了省去定义一个 Runnable 实现类的麻烦,必须使用匿名内部类
④ 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值必须再写一遍,且不能写错
⑤ 实际上,似乎只有方法体才是关键所在。
4、编程思想转换
对于上面的案例来说,我们真的希望创建一个匿名内部类对象吗?并不需要,只是为了做这件事情不得不创建一个对象。
真正希望做的是:将 run 方法体内的代码传递给 Thread 类。
传递一段代码——这才是真正的目的。而创建对象只是受限于面向对象而不得不采取的一种手段方式。
如果我们将关注点从“怎么做”回归到“做什么” 的本质上,就会发现只要能够更好地达到母的,过程与形式其实并不重要。
5、Lambda 的更优写法
JDK8 之后,有了全新的语法,上面的 Runnable 接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到等效:
1 public class Demo02LambdaRunnable {
2 public static void main(String[] args) {
3 new Thread(() ‐> System.out.println("多线程任务执行!")).start(); // 启动线程
4 }
5 }
这段代码和上面的执行效果是一样的。从代码的语义上可以看出:启动了一个线程,而线程任务的内容以一种更简洁的形式被指定。
优点:不再有“必须创建接口对象” 的束缚,不再有 “抽象方法覆盖重写” 的负担。
案例一:
案例二:
6、匿名内部类到 Lambda 的转换
Lambda 表达式是怎么做到的呢?核心代码如下:
() ‐> System.out.println("多线程任务执行!")
为了理解 Lambda 的语义,先来看一下以前的实现方式。
(1)使用实现类
要启动一个线程,需要创建一个 Thread 类的对象并调用 start 方法,而为了指定线程执行的内容,需要调用 thread 类的构造方法:
public Thread(Runnable target)
为了获取 Runnable 接口的实现对象,可以为该接口定义一个实现类 RunnableImplement:
1 public class RunnableImpl implements Runnable {
2 @Override
3 public void run() {
4 System.out.println("多线程任务执行!");
5 }
6 }
然后创建该实现类的对象作为 Thread 类的构造参数:
1 public class Demo03ThreadInitParam {
2 public static void main(String[] args) {
3 Runnable task = new RunnableImpl();
4 new Thread(task).start();
5 }
6 }
(2)使用匿名内部类
这个 RunnableImpl 类只是为了实现 Runnable 接口存在的,而且仅被使用了唯一一次,所以使用匿名内部类的语法即可省去该类的单独定义,即匿名内部类:
1 public class Demo04ThreadNameless {
2 public static void main(String[] args) {
3 new Thread(new Runnable() {
4 @Override
5 public void run() {
6 System.out.println("多线程任务执行!");
7 }
8 }).start();
9 }
10 }
(3)匿名内部类的好处与弊端
好处:匿名内部类可以省去实现类的定义;
缺点:匿名内部类语法复杂
语义分析:
经分析上述代码中的语义,Runnable 接口只有一个 run 方法的定义:
public abstract void run();
即制定了一种做事情的方案(本质为一个函数):
① 无参数:不需要任何条件即可执行该方案
② 无返回值:该方案不产生任何结果
③ 代码块(方法体):该方案的具体执行步骤
同样的语义体现在 Lambda 语法中,会更加简单:
() ‐> System.out.println("多线程任务执行!")
① 前面的一对小括号即 run 方法的参数(无),代表不需要任何条件;
② 中间的一个箭头代表前面的参数传递给后面的代码
③ 后面的输出语句即业务逻辑代
7、
二、Lambda表达式:语法
1、Lambda 表达式
Lambda 表达式: 在Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的参数列表(其实就是接口中的抽象方法的形参列表)
右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能(其实就是重写的抽象方法的方法体)
Lambda 表达式的标准格式为:
(参数类型 参数名称) ‐> { 代码语句 }
格式说明:
-
-
- 小括号内的语法与传统方法参数列表一致;无参数则留空;多个参数则用逗号分隔。
- -> 是新引入的语法格式,代表指向动作。
- 大括号的语法与传统方法要求基本一致。
-
Lambda 表达式:是可推导,可以省略,凡是根据上下文推导出来的内容,都可以省略书写。
可以省略的内容:
① (参数列表):括号中参数列表的数据类型,可以省略不写
② (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
③ {一些代码}:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号)
注意:要省略{},return,分号必须一起省略
2、Lambda 的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
(1)使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
(2)无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
(3)使用Lambda必须具有上下文推断 。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
Tips:有且仅有一个抽象方法的接口,称为“函数式接口”。
3、Lambda表达式的本质:作为函数式接口的实例
三、Lambda 表达式的使用
1、语法格式一:无参,无返回值
Demo:
1 @Test
2 public void test1(){
3 Runnable r1 = new Runnable() {
4 @Override
5 public void run() {
6 System.out.println("Hello World");
7 }
8 };
9
10 r1.run();
11
12 System.out.println("***********************");
13
14 Runnable r2 = () -> {
15 System.out.println("Hello Java");
16 };
17
18 r2.run();
19 }
2、语法格式二:Lambda 需要一个参数,但是没有返回值
Demo:
1 @Test
2 public void test2(){
3
4 Consumer<String> con = new Consumer<String>() {
5 @Override
6 public void accept(String s) {
7 System.out.println(s);
8 }
9 };
10 con.accept("谎言和誓言的区别是什么?");
11
12 System.out.println("*******************");
13
14 Consumer<String> con1 = (String s) -> {
15 System.out.println(s);
16 };
17 con1.accept("一个是听得人当真了,一个是说的人当真了");
18
19 }
3、语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
Demo:
1 @Test
2 public void test3(){
3
4 Consumer<String> con1 = (String s) -> {
5 System.out.println(s);
6 };
7 con1.accept("一个是听得人当真了,一个是说的人当真了");
8
9 System.out.println("*******************");
10
11 Consumer<String> con2 = (s) -> {
12 System.out.println(s);
13 };
14 con2.accept("一个是听得人当真了,一个是说的人当真了");
15
16 }
4、语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
Demo:
1 @Test
2 public void test4(){
3 Consumer<String> con1 = (s) -> {
4 System.out.println(s);
5 };
6 con1.accept("一个是听得人当真了,一个是说的人当真了");
7
8 System.out.println("*******************");
9
10 Consumer<String> con2 = s -> {
11 System.out.println(s);
12 };
13 con2.accept("一个是听得人当真了,一个是说的人当真了");
14
15 }
5、语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
Demo:
1 @Test
2 public void test5(){
3
4 Comparator<Integer> com1 = new Comparator<Integer>() {
5 @Override
6 public int compare(Integer o1, Integer o2) {
7 System.out.println(o1);
8 System.out.println(o2);
9 return o1.compareTo(o2);
10 }
11 };
12
13 System.out.println(com1.compare(12,21));
14
15 System.out.println("*****************************");
16 Comparator<Integer> com2 = (o1,o2) -> {
17 System.out.println(o1);
18 System.out.println(o2);
19 return o1.compareTo(o2);
20 };
21
22 System.out.println(com2.compare(12,6));
23
24 }
6、语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
Demo:
1 @Test
2 public void test6(){
3
4 Comparator<Integer> com1 = (o1,o2) -> {
5 return o1.compareTo(o2);
6 };
7
8 System.out.println(com1.compare(12,6));
9
10 System.out.println("*****************************");
11
12 Comparator<Integer> com2 = (o1,o2) -> o1.compareTo(o2);
13
14 System.out.println(com2.compare(12,21));
15
16 }
7、总结
->左边:lambda 形参列表的参数类型可以省略(类型推断);如果 lambda 形参列表只有一个参数,其一对()也可以省略
->右边:lambda 体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字
四、Lambda 中的类型推断
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断” 。
五、