3.1 函数式编程思想概述
在数学中, 函数就是有输入量, 输出量的一套计算方案, 也就是 "拿什么东西做什么事情". 相对而言, 面向对象过分强调 "必须通过对象的形式来做事情", 而函数式思想则尽量忽略面向对象的复杂语法 -- 强调做什么, 而不是以什么形式做.
面向对象的思想:
做一件事情, 找一个能解决这个事情的对象, 调用对象的方法, 完成事情.
函数式编程思想:
只要能获取到结果, 谁去做的, 怎么做的都不重要, 重视的是结果, 不重视过程
3.2 冗余的 Runnable 代码
传统写法
当需要启动一个线程去完成任务时, 通常会通过 java.lang.Runnable 接口来定义任务内容, 并使用 java.lang.Thread 类来启动该线程. 代码如下:
- public class Demo01Runnable {
- public static void main(String[] args) {
- // 匿名内部类
- Runnable task = new Runnable() {
- @Override
- public void run() { // 覆盖重写抽象方法
- System.out.println("多线程任务执行!");
- }
- };
- new Thread(task).start(); // 启动线程
- }
- }
本着 "一切皆对象" 的思想, 这种做法是无可厚非的: 首先创建一个 Runnable 接口的匿名内部类对象来指定任务内容, 再将其交给一个线程来启动.
代码分析
对于 Runnable 的匿名内部类用法, 可以分析出几点内容:
Thread 类需要 Runnable 接口作为参数, 其中的抽象 run 方法是用来指定线程任务内容的核心;
为了指定 run 的方法体, 不得不需要 Runnable 接口的实现类;
为了省去定义一个 RunnableImpl 实现类的麻烦, 不得不使用匿名内部类;
必须覆盖重写抽象 run 方法, 所以方法名称, 方法参数, 方法返回值不得不再写一遍, 且不能写错;
而实际上, 似乎只有方法体才是关键所在.
3.3 编程思想转换
做什么, 而不是怎么做
我们真的希望创建一个匿名内部类对象吗? 不. 我们只是为了做这件事情而不得不创建一个对象. 我们真正希望做的事情是: 将 run 方法体内的代码传递给 Thread 类知晓.
传递一段代码 -- 这才是我们真正的目的. 而创建对象只是受限于面向对象语法而不得不采取的一种手段方式. 那有没有更加简单的办法? 如果我们将关注点从 "怎么做" 回归到 "做什么" 的本质上, 就会发现只要能够更好地达到目的, 过程与形式其实并不重要.
3.4 体验 Lambda 的更优写法
借助 Java 8 的全新语法, 上述 Runnable 接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到等效:
- public class Demo02LambdaRunnable {
- public static void main(String[] args) {
- new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
- }
- }
这段代码和刚才的执行效果是完全一样的, 可以在 1.8 或更高的编译级别下通过. 从代码的语义中可以看出: 我们启动了一个线程, 而线程任务的内容以一种更加简洁的形式被指定.
不再有 "不得不创建接口对象" 的束缚, 不再有 "抽象方法覆盖重写" 的负担, 就是这么简单!
3.5 回顾匿名内部类
Lambda 是怎样击败面向对象的? 在上例中, 核心代码其实只是如下所示的内容:
() -> System.out.println("多线程任务执行!")
为了理解 Lambda 的语义, 我们需要从传统的代码起步.
使用实现类
要启动一个线程, 需要创建一个 Thread 类的对象并调用 start 方法. 而为了指定线程执行的内容, 需要调用 Thread 类的构造方法:
public Thread(Runnable target)
为了获取 Runnable 接口的实现对象, 可以为该接口定义一个实现类 RunnableImpl :
- public class RunnableImpl implements Runnable {
- @Override
- public void run() {
- System.out.println("多线程任务执行!");
- }
- }
然后创建该实现类的对象作为 Thread 类的构造参数:
- public class Demo03ThreadInitParam {
- public static void main(String[] args) {
- Runnable task = new RunnableImpl();
- new Thread(task).start();
- }
- }
使用匿名内部类
这个 RunnableImpl 类只是为了实现 Runnable 接口而存在的, 而且仅被使用了唯一一次, 所以使用匿名内部类的语法即可省去该类的单独定义, 即匿名内部类:
- public class Demo04ThreadNameless {
- public static void main(String[] args) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("多线程任务执行!");
- }
- }).start();
- }
- }
匿名内部类的好处与弊端
一方面, 匿名内部类可以帮我们省去实现类的定义; 另一方面, 匿名内部类的语法 -- 确实太复杂了!
语义分析
仔细分析该代码中的语义, Runnable 接口只有一个 run 方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
无参数: 不需要任何条件即可执行该方案.
无返回值: 该方案不产生任何结果.
代码块(方法体): 该方案的具体执行步骤.
同样的语义体现在 Lambda 语法中, 要更加简单:
() -> System.out.println("多线程任务执行!")
前面的一对小括号即 run 方法的参数(无), 代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码.
3.6 Lambda 标准格式
Lambda 省去面向对象的条条框框, 格式由 3 个部分组成:
一些参数
一个箭头
一段代码
Lambda 表达式的标准格式为:
(参数列表) -> {一些重写方法的代码};
解释说明格式:
(): 接口中抽象方法的参数列表, 没有参数, 就空着; 有参数就写出参数, 多个参数使用逗号分隔
->: 传递的意思, 把参数传递给方法体{}
{}: 重写接口的抽象方法的方法体
代码示例:
- /*
- Lambda 表达式的标准格式:
- 由三部分组成:
- a. 一些参数
- b. 一个箭头
- c. 一段代码
- 格式:
- (参数列表) -> {一些重写方法的代码};
- 解释说明格式:
- (): 接口中抽象方法的参数列表, 没有参数, 就空着; 有参数就写出参数, 多个参数使用逗号分隔
- ->: 传递的意思, 把参数传递给方法体{}
- {}: 重写接口的抽象方法的方法体
- */
- public class Demo02Lambda {
- public static void main(String[] args) {
- // 使用匿名内部类的方式, 实现多线程
- new Thread(new Runnable(){
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName()+"新线程创建了");
- }
- }).start();
- // 使用 Lambda 表达式, 实现多线程
- new Thread(()->{
- System.out.println(Thread.currentThread().getName()+"新线程创建了");
- }
- ).start();
- // 优化省略 Lambda
- new Thread(()->System.out.println(Thread.currentThread().getName()+"新线程创建了")).start();
- }
- }
3.7 练习: 使用 Lambda 标准格式(无参无返回)
题目
给定一个厨子 Cook 接口, 内含唯一的抽象方法 makeFood , 且无参数, 无返回值
- Cook.java:
- /*
- 定一个厨子 Cook 接口, 内含唯一的抽象方法 makeFood
- */
- public interface Cook {
- // 定义无参数无返回值的方法 makeFood
- public abstract void makeFood();
- }
- Demo01Cook.java:
- /*
- 需求:
- 给定一个厨子 Cook 接口, 内含唯一的抽象方法 makeFood, 且无参数, 无返回值.
- 使用 Lambda 的标准格式调用 invokeCook 方法, 打印输出 "吃饭啦!" 字样
- */
- public class Demo01Cook {
- public static void main(String[] args) {
- // 调用 invokeCook 方法, 参数是 Cook 接口, 传递 Cook 接口的匿名内部类对象
- invokeCook(new Cook() {
- @Override
- public void makeFood() {
- System.out.println("吃饭了");
- }
- });
- // 使用 Lambda 表达式, 简化匿名内部类的书写
- invokeCook(()->{
- System.out.println("吃饭了");
- });
- // 优化省略 Lambda
- invokeCook(()-> System.out.println("吃饭了"));
- }
- // 定义一个方法, 参数传递 Cook 接口, 方法内部调用 Cook 接口中的方法 makeFood
- public static void invokeCook(Cook cook){
- cook.makeFood();
- }
- }
备注: 小括号代表 Cook 接口 makeFood 抽象方法的参数为空, 大括号代表 makeFood 的方法体.
3.8 Lambda 的参数和返回值
需求:
使用数组存储多个 Person 对象
对数组中的 Person 对象使用 Arrays 的 sort 方法通过年龄进行升序排序
下面举例演示 java.util.Comparator<T> 接口的使用场景代码, 其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时, Arrays.sort 方法需要一个 Comparator 接口实例来指定排序的规则. 假设有一个 Person 类, 含有 String name 和 int age 两个成员变量:
- Person.java:
- public class Person {
- private String name;
- private int age;
- public Person() {
- }
- public Person(String name, int age) {
- this.name = name;
- this.age = age;
- }
- @Override
- public String toString() {
- return "Person{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- }
传统写法
如果使用传统的代码对 Person[] 数组进行排序, 写法如下:
- import java.util.Arrays;
- import java.util.Comparator;
- public class Demo06Comparator {
- public static void main(String[] args) {
- // 本来年龄乱序的对象数组
- Person[] array = {
- new Person("古力娜扎", 19),
- new Person("迪丽热巴", 18),
- new Person("马尔扎哈", 20)};
- // 匿名内部类
- Comparator<Person> comp = new Comparator<Person>() {
- @Override
- public int compare(Person o1, Person o2) {
- return o1.getAge() -o2.getAge();
- }
- };
- Arrays.sort(array, comp); // 第二个参数为排序规则, 即 Comparator 接口实例
- for (Person person : array) {
- System.out.println(person);
- }
- }
- }
这种做法在面向对象的思想中, 似乎也是 "理所当然" 的. 其中 Comparator 接口的实例 (使用了匿名内部类) 代表了 "按照年龄从小到大" 的排序规则.
代码分析
下面我们来搞清楚上述代码真正要做什么事情.
为了排序, Arrays.sort 方法需要排序规则, 即 Comparator 接口的实例, 抽象方法 compare 是关键;
为了指定 compare 的方法体, 不得不需要 Comparator 接口的实现类;
为了省去定义一个 ComparatorImpl 实现类的麻烦, 不得不使用匿名内部类;
必须覆盖重写抽象 compare 方法, 所以方法名称, 方法参数, 方法返回值不得不再写一遍, 且不能写错;
实际上, 只有参数和方法体才是关键.
Lambda 写法
- import java.util.Arrays;
- public class Demo07ComparatorLambda {
- public static void main(String[] args) {
- Person[] array = {
- new Person("古力娜扎", 19),
- new Person("迪丽热巴", 18),
- new Person("马尔扎哈", 20)};
- Arrays.sort(array, (Person a, Person b) ->{
- return a.getAge() -b.getAge();
- });
- for (Person person : array) {
- System.out.println(person);
- }
- }
- }
3.9 练习: 使用 Lambda 标准格式(有参有返回)
题目
给定一个计算器 Calculator 接口, 内含抽象方法 calc 可以将两个 int 数字相加得到和值:
- Calculator.java:
- /*
- 给定一个计算器 Calculator 接口, 内含抽象方法 calc 可以将两个 int 数字相加得到和值
- */
- public interface Calculator {
- // 定义一个计算两个 int 整数和的方法并返回结果
- public abstract int calc(int a,int b);
- }
- Demo01Calculator.java:
- /*
- Lambda 表达式有参数有返回值的练习
- 需求:
- 给定一个计算器 Calculator 接口, 内含抽象方法 calc 可以将两个 int 数字相加得到和值
- 使用 Lambda 的标准格式调用 invokeCalc 方法, 完成 120 和 130 的相加计算
- */
- public class Demo01Calculator {
- public static void main(String[] args) {
- // 调用 invokeCalc 方法, 方法的参数是一个接口, 可以使用匿名内部类
- invokeCalc(10, 20, new Calculator() {
- @Override
- public int calc(int a, int b) {
- return a+b;
- }
- });
- // 使用 Lambda 表达式简化匿名内部类的书写
- invokeCalc(120,130,(int a,int b)->{
- return a + b;
- });
- // 优化省略 Lambda
- invokeCalc(120,130,(a,b)-> a + b);
- }
- /*
- 定义一个方法
- 参数传递两个 int 类型的整数
- 参数传递 Calculator 接口
- 方法内部调用 Calculator 中的方法 calc 计算两个整数的和
- */
- public static void invokeCalc(int a,int b,Calculator c){
- int sum = c.calc(a,b);
- System.out.println(sum);
- }
- }
备注: 小括号代表 Calculator 接口 calc 抽象方法的参数, 大括号代表 calc 的方法体.
3.10 Lambda 省略格式
Lambda 表达式: 是可推导, 可以省略
凡是根据上下文推导出来的内容, 都可以省略书写
可以省略的内容:
(参数列表): 括号中参数列表的数据类型, 可以省略不写
(参数列表): 括号中的参数如果只有一个, 那么类型和 () 都可以省略
{一些代码}: 如果 {} 中的代码只有一行, 无论是否有返回值, 都可以省略({},return, 分号)
注意: 要省略{},return, 分号必须一起省略
补充:
- public class Demo01ArrayList {
- public static void main(String[] args) {
- //JDK1.7 版本之前, 创建集合对象必须把前后的泛型都写上
- ArrayList<String> list01 = new ArrayList<String>();
- //JDK1.7 版本之后,= 号后边的泛型可以省略, 后边的泛型可以根据前边的泛型推导出来
- ArrayList<String> list02 = new ArrayList<>();
- }
- }
3.11 练习: 使用 Lambda 省略格式
题目
仍然使用前文含有唯一 makeFood 抽象方法的厨子 Cook 接口, 在下面的代码中, 请使用 Lambda 的省略格式调用 invokeCook 方法, 打印输出 "吃饭啦!" 字样:
- public class Demo09InvokeCook {
- public static void main(String[] args) {
- // TODO 请在此使用 Lambda[省略格式] 调用 invokeCook 方法
- }
- private static void invokeCook(Cook cook) {
- cook.makeFood();
- }
- }
解答
- public static void main(String[] args) {
- invokeCook(() -> System.out.println("吃饭啦!"));
- }
3.12 Lambda 的使用前提
Lambda 的语法非常简洁, 完全没有面向对象复杂的束缚. 但是使用时有几个问题需要特别注意:
1. 使用 Lambda 必须具有接口, 且要求接口中有且仅有一个抽象方法.
无论是 JDK 内置的 Runnable ,Comparator 接口还是自定义的接口, 只有当接口中的抽象方法存在且唯一时, 才可以使用 Lambda.
2. 使用 Lambda 必须具有上下文推断.
也就是方法的参数或局部变量类型必须为 Lambda 对应的接口类型, 才能使用 Lambda 作为该接口的实例.
备注: 有且仅有一个抽象方法的接口, 称为 "函数式接口".
来源: http://www.bubuko.com/infodetail-3189377.html