java之Lambda表达式
写在前面
lambda 表达式是一个可传递的代码块
, 可以在以后执行一次或多次。
函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。
相对而言,面向对象过分强调“必须通过对象的形式来做事情”。
函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
面向对象的思想:
- 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
函数式编程思想:
- 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
举例
以一个简单的实现Runnable接口的线程类为例:
面向对象方式
/**
* 标准方式(面向对象)创建线程
* @author layman
*/
public class Demo01 {
public static void main(String[] args) {
//标准方式创建线程
Demo01Runnable run = new Demo01Runnable();
Thread t = new Thread(run);
t.start();
}
}
class Demo01Runnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--线程任务开启");
}
}
匿名内部类方式
/**
* 匿名内部类方式创建线程
* @author layman
*/
public class Demo01 {
public static void main(String[] args) {
//匿名内部类方式创建线程
Runnable run = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--线程任务开启");
}
};
new Thread(run ).start();
}
}
可以看到,通过匿名内部类,可以大大的简化代码。
可以看到这个匿名内部类仅被使用了一次,因此无需使用参数接收这个匿名内部类,而是直接把它作为参数传递。
本着这样的思考,我们可以继续精简代码,最终如下:
/**
* 匿名内部类方式创建线程(精简)
* @author layman
*/
public class Demo01 {
public static void main(String[] args) {
//匿名内部类方式创建线程(精简)
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--线程任务开启");
}
}).start();
}
}
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
匿名内部类的好处与弊端
一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!
代码分析
对于Runnable的匿名内部类用法,可以分析出几点内容:
- Thread类需要Runnable接口作为参数,只有其中的抽象run方法是核心。
- 为了指定run的方法体,必须指定Runnable接口的实现类。
- 为了省去定义一个Runnable接口实现类的麻烦,不得不使用匿名内部类。
- 必须覆盖重写抽象run方法。
然鹅,实际上,似乎只有方法体才是关键。
编程思想转换
做什么,而不是怎么做
- 我们真的希望创建一个匿名内部类对象吗?
- 不。我们只是为了做这件事情而不得不创建一个对象。
- 我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。
传递代码
——这才是我们真正的目的。
创建对象只是受限于面向对象语法而不得不采取的一种手段方式。
那,有没有更加简单的办法?
如果我们将关注点从怎么做回归到做什么的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
开启新知识的大门(Lambda表达式)
借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
/**
* @author layman
*/
public class Demo01Lambda {
public static void main(String[] args) {
//Lambda表达式
new Thread(() -> System.out.println(Thread.currentThread().getName()+"--线程任务开启")).start();
}
}
这段代码和刚才的执行效果是完全一样的,可以在jdk1.8或更高的编译级别下通过。
代码语义分析
() -> System.out.println(Thread.currentThread().getName()+"--线程任务开启")
- 前面的一对小括号
()
是即run方法的参数,上面的表达式表示没有参数。 - 中间的一个箭头
->
代表将前面的参数传递给后面的代码; - 后面的输出语句是业务逻辑代码。
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有不得不创建接口对象的束缚,不再有抽象方法覆盖重写的负担,就是这么简单!
lambda 表达式形式
参数, 箭头(->) 以及一个表达式。
(参数类型 参数名称) -> { 代码语句 }
如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 {}中,并包含显式的 return语句。例如:
(String first, String second) -> {
if (first.length() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0;
}
即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样:
() -> { for (int i = 0;i > 100;i++ ) System.out.println(i); }
如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。例如:
// 等同于(String first, String second)
Comparator<String> comp= (first, second) -> first.length() - second.length();
如果方法只有一 参数, 而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
// 不是(event) -> . . . 或者 (ActionEvent event) -> .
ActionListener listener = 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 表达
式。这种接口称为函数式接口 ( functional interface )。
简单代码示例
下面给出几个简单的代码示例
简单练习A (无参无返回)
定义一个Hero类,定义一个抽象方法hitMonster(),进行调用。
代码如下:
/**
* @author layman
*/
public class Demo02 {
public static void main(String[] args) {
//匿名内部类方法调用
showTime(new Hero() {
@Override
public void hitMonster() {
System.out.println("绿巨人在揍钢铁侠");
}
});
//使用lambda表达式调用
showTime(()->{
System.out.println("葫芦娃大战奥特曼");
System.out.println("孙悟空三打白骨精");
});
}
public static void showTime(Hero hero){
hero.hitMonster();
}
}
//定义一个接口
interface Hero{
// 定义一个抽象方法
void hitMonster();
}
简单练习B (有参无返回)
需求:
使用数组存储多个Monster对象
对数组中的Monster对象使用Arrays的sort方法通过年龄进行降序排序
传统写法:
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Arrays;
import java.util.Comparator;
/**
* @author layman
*/
public class Demo03 {
public static void main(String[] args) {
//构建怪物数组
Monster[] monsters = {
new Monster("虚空怪物",300),
new Monster("触手蟑螂",45),
new Monster("铁头鱼",458)};
System.out.println("排序前:" + Arrays.toString(monsters));
//使用匿名内部类定义一个比较器
Comparator comparator = new Comparator<Monster>(){
@Override
public int compare(Monster o1, Monster o2) {
//降序
return o2.getAge() - o1.getAge();
}
};
//调用方法进行排序
Arrays.sort(monsters,comparator);
System.out.println("排序后:" + Arrays.toString(monsters));
}
}
@Data
@AllArgsConstructor
class Monster{
private String name;
private Integer age;
}
这种做法在面向对象的思想中,似乎是理所当然的。其中Comparator接口的实例(使用了匿名内部类)实现了按照年龄从大到小的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
- Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键。
- 为了指定compare的方法体,不得不需要Comparator接口的实现类。
- 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类。
- 必须覆盖重写抽象compare方法,方法名称、方法参数、方法返回值不得不再写一遍,且不能写错。
实际上,只有参数和方法体才是关键。
使用Lambda表达式优化代码:
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Arrays;
/**
* @author layman
*/
public class Demo03 {
public static void main(String[] args) {
//构建怪物数组
Monster[] monsters = {
new Monster("虚空怪物",300),
new Monster("触手蟑螂",45),
new Monster("铁头鱼",458)};
System.out.println("排序前:" + Arrays.toString(monsters));
//使用Lambda表达式编写函数接口
Arrays.sort(monsters,(o1,o2)->{
return o2.getAge() - o1.getAge();
});
System.out.println("排序后:" + Arrays.toString(monsters));
}
}
@Data
@AllArgsConstructor
class Monster{
private String name;
private Integer age;
}
简单练习C (有参有返回)
给定一个计算器Calculator接口,内含抽象方法getSum可以将两个int数字相加得到和:
代码如下:
/**
* @author layman
* @date 2021/3/2
*/
public class Demo04 {
public static void main(String[] args) {
//匿名内部类写法
int sum = getSum(10, 20, new Calculator() {
@Override
public int getSum(int a, int b) {
return a + b;
}
});
System.out.println("求和sum为:" + sum);
//标准lambda表达式写法
int sum1 = getSum(30, 40, (int a, int b) -> {
return a + b;
}
);
System.out.println("求和sum1为:" + sum1);
//lambda表达式省略写法
int sum2 = getSum(50, 60, (a, b) ->
a + b
);
System.out.println("求和sum2为:" + sum2);
}
public static int getSum(int a,int b,Calculator calculator){
int sum = calculator.getSum(a,b);
return sum;
}
}
interface Calculator {
int getSum(int a,int b);
}
Lambda省略格式
原则:可推导即可省略
省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
- 如果参数类型可以由上下文推导得出,那么小括号内的参数类型可以省略不写。
- 如果小括号内有且仅有一个参数,那么小括号可以省略。
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的Runnable、Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
- 使用Lambda必须具有上下文推断。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。