说来惭愧,很早就想写一篇 Lambda 表达式的博客了,算下来这篇博客应该拖了快一年了吧。
一、Lambda 表达式介绍
1.1 Lambda 表达式概念
Lamada 表达式是 Java8 的一个新特性,也是 Java8 中最值得学习的新特性之一。
Lambda 表达式本质上是一个匿名函数。我们可以使用这个匿名函数,来实现接口中的方法。使用 Lambda 表达式可以写出更简洁、更灵活的代码。
1.2 Lambda 表达式的使用场景
通常来说,使用 Lambda 表达式,就是为了简化接口的实现。
关于接口的实现,可以有很多种方式来实现。例如:单独写个类去实现接口、使用匿名内部类。但是 Lambda 表达式,比这两种方式都简单。
口说无凭,下面用代码来验证一下。
假设现在,我有一个需求:实现一个 Comparator
接口,按照以前学习过的方法,一般是这么来写匿名内部类的:
public void test1(){
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
System.out.println(comparator.compare(2, 10));
}
现在,引入了 Lambda 表达式之后,写法变成了这样:
public void test2(){
Comparator<Integer> comparator = (o1, o2) -> Integer.compare(o1, o2);
System.out.println(comparator.compare(2, 10));
}
从上面可以看出,相对于原先直接写匿名内部类,使用 Lambda 表达式的写法非常简洁。
1.3 Lambda 表达式对接口的要求
虽然 Lambda 表达式可以在一定程度上简化接口的实现。但是,并不是所有的接口都可以使用 Lambda 表达式来简洁实现的。Lambda 表达式毕竟只是一个匿名方法,当实现的接口中的方法过多或者过少的时候,Lambda 表达式都是不适用的。
Lambda 表达式的使用场景归结来说就是一句话:Lambda 表达式只能实现函数式接口。
1.4 函数式接口
1.4.1 基础概念
对于一个接口,如果它要求实现类必须实现的抽象方法有且只有一个。这样的接口,就是函数式接口。
【举例说明】
-
例一:接口 A 定义如下:
interface A{ void test(); }
如上 A 接口,它要求实现类必须实现的抽象方法只有一个,那么它就是一个函数式接口。
-
例二:接口 B 定义如下:
interface B{ void test1(); void test2(); }
如上 B 接口,它的实现类必须要实现两个抽象方法,因此它不是一个函数式接口。
-
例三:我们难度升级一下,来看看接口 C:
interface C extends A{ }
如上 C 接口,虽然接口 C 没有定义任何方法,但是它可以从父接口中继承到唯一的一个抽象方法。因此它是一个函数式接口。
-
例四:对于接口 D:
interface D{ void test1(); default void test2(){} }
虽然对于接口 D 中有两个方法,但是对于
default
方法,并不要求子类必须实现。因此该接口是一个函数式接口。
1.4.2 判别函数式接口
我们可以通过借助注解 @FunctionalInterface
来判断某个接口是否是函数式接口。
具体使用方法为:将该注解标识在接口上即可。如果该接口是函数式接口,则没有任何现象产生;如果该接口不是函数式接口,那么则会报错。
【举例说明】
使用注解判别 Test
接口是否是函数式接口:
@FunctionalInterface
public interface Test{
void test1();
void test2();
}
由于该接口要求实现类必须实现的抽象方法是两个,因此编辑器将会报错:
二、 Lambda 表达式的语法
2.1 Lambda 表达式的基础语法
【基本语法】
Lambda 表达式的基本语法如下:
(参数) -> {
方法体
};
表达式各部分作用如下:
->:用来分隔参数和方法体。
参数部分:方法的参数列表,要求和实现的接口中的方法参数部分一致,包括参数的数量和类型。
方法体部分:方法的实现部分,如果接口中定义的方法有返回值,则是在实现的时候,注意返回值的返回。
从 Lambda 表达式的基础语法可以看出,我们在写 Lambda 表达式的时候,只需要关注两部分内容:参数列表 和 方法体。
Lambda 表达式本质上来说就是一个匿名函数。因此我们在写 Lambda 表达式的时候,不需要关心方法名到底是什么,也不用关心返回值类型是什么。
光说不练假把式,下面就使用案例来演示一下 Lambda 表达式的基本使用。
【举例说明】
我们定义下面四个函数式接口:
/**
* @Description 无参数无返回值函数式接口
*/
public interface NoneParamNoneReturn {
void test();
}
/**
* @Description 一个参数一个返回值的函数式接口
*/
public interface OneParamOneReturn {
Integer test(int a);
}
/**
* @Description 一个参数无返回值的函数式接口
*/
public interface OneParamNoneReturn {
void test(int a);
}
/**
* @Description 两个参数,一个返回值的函数式接口
*/
public interface TwoParamOneReturn {
Integer test(int a, int b);
}
根据 Lambda 表达式的基础语法,我们使用其来实现这四个函数式接口的代码如下:
public class LambdaDemo_02 {
public static void main(String[] args) {
// 1. 实现无参、无返回值接口
NoneParamNoneReturn noneParamNoneReturn = () -> {
System.out.println("这是无参数、无返回值的方法");
};
noneParamNoneReturn.test();
// 2. 实现单个参数、无返回值接口
OneParamNoneReturn oneParamNoneReturn = (int a)->{
System.out.println("这是单个参数、无返回值的方法");
System.out.println("参数是:" + a);
};
oneParamNoneReturn.test(20);
// 3. 实现单个参数、有返回值接口
OneParamOneReturn oneParamOneReturn = (int a)->{
System.out.println("这是单个参数,有返回值的方法");
return a + 10;
};
System.out.println("返回值是:" + oneParamOneReturn.test(20));
// 4. 实现两个参数,有返回值接口
TwoParamOneReturn twoParamOneReturn = (int a, int b)->{
System.out.println("这是两个参数、有返回值的方法");
return a + b;
};
System.out.println("返回值是" + twoParamOneReturn.test(10, 20));
}
}
2.2 Lambda 表达式的语法进阶
在上述代码中,的确可以使用 Lambda 表达式来实现接口,但是其语法仍不够简洁,仍然有优化的空间。
2.2.1 参数部分的精简
2.2.1.1 参数类型的精简
由于在接口的方法中已经定义了每一个参数的类型是什么,且在使用 Lambda 表达式实现接口的时候,必须要保证参数的数量和类型需要和接口中的方法保持一致。因此,此时 Lambda 表达式中的参数的类型可以省略不写。
【注意】
如果要省略参数的类型,需要保证:每一个参数的类型都必须省略。决不能出现有的参数类型省略,有的参数类型没有省略的情况。
【举例说明】
使用参数精简的 Lambda 表达式实现两个参数的函数式接口:
// 原 lambda 实现
TwoParamOneReturn twoParamOneReturn = (int a, int b)->{
System.out.println("这是两个参数、有返回值的方法");
return a + b;
};
// 参数类型精简 lambda 实现
TwoParamOneReturn twoParamOneReturn = (a, b)->{ // 参数类型可以省略
System.out.println("这是两个参数、有返回值的方法");
return a + b;
};
2.2.1.2 参数括号的精简
如果方法的参数列表中的参数数量有且只有一个,那么参数列表的小括号是可以省略不写的。
【注意】
-
只有当参数的个数是一个的时候才能省略小括号
-
省略掉小括号时,必须省略参数类型
【举例说明】
使用参数精简的 Lambda 表达式实现单个参数的函数式接口:
// 原 lambda 实现
OneParamNoneReturn oneParamNoneReturn = (int a)->{
System.out.println("这是单个参数、无返回值的方法");
System.out.println("参数是:" + a);
};
// 参数括号精简 lambda 实现
OneParamNoneReturn oneParamNoneReturn = a->{ // 单个参数,括号、参数类型可以省略
System.out.println("这是单个参数、无返回值的方法");
System.out.println("参数是:" + a);
};
2.2.2 方法体部分的精简
2.2.2.1 方法体大括号的精简
当一个方法体中的语句有且只有一句的情况下,大括号可以省略。
【举例说明】
// 原 lambda 实现
OneParamNoneReturn oneParamNoneReturn = a -> {
System.out.println("参数a=" + a);
};
// 大括号精简 lambda 实现
OneParamNoneReturn oneParamNoneReturn = a -> System.out.println("参数a=" + a); // 大括号可省略
2.2.2.2 return 的精简
如果方法体中唯一的一条语句是返回语句,在省略大括号的同时,还可以省略 return
关键字。
【举例说明】
// 原 lambda 实现
TwoParamOneReturn twoParamOneReturn = (a, b)->{ // 参数类型可以省略
return a - b;
};
// 大括号精简 lambda 实现
TwoParamOneReturn twoParamOneReturn = (a, b) -> a - b; // return 可以省略
三、函数引用
函数引用,即引用一个已经存在的方法,使其替代 Lambda 表达式来完成接口的实现。
Lambda 表达式是为了简化接口实现的,因此在 Lambda 表达式中,不应该出现比较复杂的逻辑。如果在 Lambda 表达式中出现了过于复杂的逻辑,会对程序的可读性造成较大的影响。
如果在 Lambda 表达式中需要处理的逻辑比较复杂,一般情况下会单独写一个方法,然后在 Lambda 表达式中引用这个方法即可。
3.1 静态方法的引用
【基本语法】
类::静态方法
【注意事项】
- 在引用的方法后面,不要添加小括号。
- 引用的这个方法,参数(数量、类型)和返回值类型,必须要跟接口中定义的一致。
【举例说明】
Lambda 表达式引用一个与函数式接口方法参数、返回值完全一致的静态方法:
public class LambdaDemo_05 {
// 函数式接口
public interface Calculator{
int calculate(int a, int b);
}
// 对 a、b 做运算的方法
public static int calculate(int a, int b){
if (a > b){
return a - 10;
}else if (a < b){
return a + 10;
}
return a + b;
}
public static void main(String[] args) {
/* 由于有现成的符合需求的静态方法,所以可以直接引用 */
Calculator calculator = LambdaDemo_05::calculate; // 引用静态方法
System.out.println(calculator.calculate(10, 20));
}
}
3.2 非静态方法的引用
【基本语法】
对象::非静态方法
【注意事项】
- 在引用的方法后面,不要添加小括号。
- 引用的这个方法,参数(类型、数量)和返回值类型,必须和接口中定义的方法一致。
【举例说明】
Lambda 表达式引用一个与函数式接口方法参数、返回值完全一致的非静态方法:
/**
* @Description lambda 表达式的函数引用
* @Date 2020/12/8 10:54
*/
public class LambdaDemo_05 {
// 函数式接口
public interface Calculator{
int calculate(int a, int b);
}
public static void main(String[] args) {
Calculator calculator = new LambdaDemo_05()::calculate_1; // 使用 对象::非静态方法
System.out.println(calculator.calculate(10, 20));
}
// 非静态方法,参数个数、类型,返回值类型与函数式接口方法一致
public int calculate_1(int a, int b){
if (a > b){
return a;
}else {
return a + 20;
}
}
}
3.3 构造方法的引用
如果某个函数式接口中定义的方法,仅仅是为了得到一个类的对象。此时我们就可以使用构造方法的引用来简化这个方法的实现。
【基本语法】
类名::new
【注意事项】
- 可以通过接口中的方法的参数,来区分不同的构造方法。
【举例说明】
首先定义一个实体类 Student
,类对象包含无参构造和有参构造:
public class Student implements Serializable {
private String name;
private Integer age;
public Student(){
System.out.println("无参构造方法");
}
public Student(String name, Integer age){
System.out.println("有参构造方法");
}
}
分别定义两个函数式接口,两个接口中唯一的方法就是用来获取 Student
对象:
// 函数式接口一
public interface StuNoneParam {
Student get(); // 无参构造
}
// 函数式接口二
public interface StuTwoParam {
Student get(String name, Integer age); // 有参构造
}
在测试类中分别引用无参构造、有参构造:
public class LambdaDemo_06 {
public static void main(String[] args) {
StuNoneParam stuNoneParam = Student::new; // 类名::new 引用构造方法
StuTwoParam stuTwoParam = Student::new;
Student student_1 = stuNoneParam.get(); // 引用无参构造
Student student_2 = stuTwoParam.get("李四", 22); // 引用有参构造
}
}
从上面可以看到,使用构造方法对 Lambda 表达式的匿名函数的实现,都是 类名::new
的形式,只是在实际引用构造方法时,针对不同的构造函数传递对应的参数即可做出区分。
3.4 对象方法的特殊引用
如果 Lambda 表达式的参数中包含某一个对象,且方法体中仅直接使用这个对象来调用它的某个方法就可以完成整体的逻辑,Lambda 表达式的其他参数作为该对象调用方法的参数。那么此时,可以对这种实现进行简化。
【举例说明】
首先定义一个实体类 Student
:
public class Student implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后分别定义两个接口 StuA
、StuB
:
public interface StuA {
/* 获取学生姓名 */
String getName(Student student); // 只有一个参数,且参数为对象
}
public interface StuB {
/* 为学生设置姓名 */
void setName(Student student, String name); // 多个参数
}
在测试类中分别引用 Student
类的 getName
、setName
方法:
public class LambdaDemo_07 {
public static void main(String[] args) {
Student student = new Student();
student.setName("泰斯特");
// 1. 单个参数
// 普通 lambda 表达式写法
StuA stuA = x -> x.getName();
// 简化后的 lambda 表达式写法
StuA stuA1 = Student::getName;
String stuA1Name = stuA1.getName(student);
// 2. 多个参数
// 普通的 lambda 表达式写法
StuB stuB = (x, y) -> x.setName(y);
// 简化后的 lambda 表达式写法
StuB stuB1 = Student::setName;
stuB1.setName(student, "测试");
}
}
【参考内容】
千峰大数据——Java8 新特性