
浅谈Java 8中的Lambda表达式
浅谈Java 8中的Lambda表达式
出现场景
使用 MybatisPlus 时,出现了 LambdaQueryWrapper 这个组件
在批23级xdx作业是发现他们大量使用了Lambda表达式,面试需要
自学sa-token时,在SatokenConfigue类也出现了Lambda表达式
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能 SaInterceptor 只要注册到项目中,默认就会打开注解校验
// lambda表达式
registry.addInterceptor(new SaInterceptor(handler -> {
// 定义详细的校验规则
SaRouter.match("/match").check(r -> System.out.println("-----啦啦啦-----"));
SaRouter
.match("/**")
.notMatch("/user/doLogin")
.check(r -> StpUtil.checkLogin());
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
SaRouter.match("/ccc/**").check(r -> StpUtil.checkPermission("ccc"));
}))
.addPathPatterns("/**")
// 下面这一句就不用写了
.excludePathPatterns("/user/doLogin");
}
}
本文通过介绍函数式编程与lambda表达式,初步理解lambda表达式基本用法。
编程思想:函数式编程
现在业界最普遍使用的编程方式即命令式编程(Imperative),命令式编程最大的特点就是,你需要告诉计算机,先做什么,然后做什么,最后做什么。
而函数式编程属于声明式编程(Declarative)的一种,即你不需要告诉计算机具体怎么执行,你只需要告诉它你想要的结果,计算机自己就进行完成。
这里注意,函数式编程的“函数”指的是数学意义上的“函数”(如数学的f(x)
,不是指编程意义上的函数(如C语言 void function() {}
)
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”。
面向对象思想强调“必须通过对象的形式来做事情”,函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做”;
从初步上理解,函数式编程要有两个特征:
函数是一等公民。即函数可以被赋值给变量,并且可以像普通的数据类型一样被存入对象,存入数组,存入其它的数据类型中。
对于这个特征,JavaScript 这语言就非常适合:
// javascript 匿名函数
const add = (a, b) => a + b;
// 函数作为“对象” 存入变量中
const compute = add; // 这里没有任何意义,在实际的开发中不推荐这么写
// 存入数组中
const list: any[] = [];
list.push(compute);
在函数式编程中,特别强调纯函数这个概念,即:相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,不会改变函数外变量的内容。
函数式编程目前支持的语言:Haskell、Python、JavaScript、Java8等等。而我们要学习的Lambda表达式就是函数式思想的体现
体验 Lambda表达式
在“Java 程序设计”课程中,我们学过线程的概念与Runnable接口:
// Java 源码
@FunctionalInterface
public interface Runnable {
/**
* Runs this operation.
*/
void run();
}
public class Thread implements Runnable {
// ...
// 构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 启动线程
public synchronized void start() {
//...
}
}
现在我们的需求为:启动一个线程,在控制台输出一句话:多线程程序启动了。
方式一:定义一个类MyRunnable实现Runnable接口,重写run()方法
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("程序启动!");
}
}
public class LambdaDemo {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
以上就是像往常一样,按照面向对象的方式理解的代码。
不过我们再回头看我们的需求,仔细想想,有没有可能其实我们只关注 run()
方法,而不用去写这么一堆?
方式二:使用匿名内部类的方式改进
我们将MyRunnable类注掉,然后再用MyRunnable的地方直接用new Runnable,IDEA会帮我们生成匿名内部类:
public class LambdaDemo {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类启动了!");
}
});
thread.start();
}
}
结合接口和代码,这里 run()
方法有几个很明显的特征:
方法没有传入参数
方法返回类型为 void
方法体中的内容是我们要做的内容
方式三:使用Lambda表达式优化
在IDEA内仔细观察 new Runnable
部分,会发现它是灰色的,IDEA提示:
Anonymous new Runnable() can be replaced with lambda
点击 “Replace with lambda", IDEA把 new Runnable() {...}
自动修改为了:
public class LambdaDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("匿名内部类启动了!");
});
thread.start();
}
}
Lambda表达式的代码分析
():里面没有内容,可以看成是方法形式参数为空
->:用箭头指向后面要做的事情
{ }:包含一段代码,我们称之为代码块,可以看成是方法体中的内容
在这个代码基础上,还能进一步进行优化,直接变成一行代码:
public class LambdaDemo {
public static void main(String[] args) {
new Thread(() -> System.out.println("Lambda启动了!")).start();
}
}
综合上述三个方法可以看出,当我们使用实现类的方式实现需求的时候,我们可以看到我们需要先创建一个实体类,然后将实现类的对象传进去才可以使用。
当我们使用你们匿名内部类的方法实现需求时,我们可以发现需要重写 run 方法(当我们调用其他方法,忘记去重写什么方法时会比较懵逼),相比较也是比较麻烦的。
当我们使用Lambda表达式来实现需求时,我们可以看到我们不用关心创建了什么实体类、重写了什么方法。我们只需要关心它最终要做的事情是 System.out.println("多线程程序启动了...");
Lambda 表达式的标准形式
组成Lambda表达式的三要素:形式参数,箭头,代码块
Lambda表达式的格式
格式:(形式参数) -> {代码块}
形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可;如果有一个参数,可以省略括号
->:由英文中画线和大于符号组成,固定写法。代表指向动作
代码块:是我们具体要做的事情,也就是以前我们写的方法体内容
Lambda表达式的使用前提
有一个接口
接口中有且仅有一个抽象方法
Lambda 表达式几种常见写法
无参无返回值
@FunctionalInterface
interface Printable {
void print();
}
public class PrintableDemo {
public static void main(String[] args) {
usePrintable(() -> {
System.out.println("Printable.print() 方法覆写");
});
}
private static void usePrintable(Printable printable) {
printable.print();
}
}
带参无返回值
@FunctionalInterface
interface Printable {
void print(String printStr);
}
public class PrintableDemo {
public static void main(String[] args) {
usePrintable((string) -> {
System.out.println(string + "Printable.print() 方法覆写");
});
}
private static void usePrintable(Printable printable) {
printable.print("接口方法传参;\n");
}
}
带参有返回值
@FunctionalInterface
interface Printable {
String print(String printStr);
}
public class PrintableDemo {
public static void main(String[] args) {
usePrintable((string) -> {
System.out.println(string + "Printable.print() 方法覆写");
return string + "Printable.print() 方法覆写";
});
}
private static void usePrintable(Printable printable) {
printable.print("接口方法传参;\n");
}
}
这行代码有两个问题
若将打印用的Sout语句去掉,就在IDEA中会看到return报了灰色,可简化;
Printable.print()
方法的返回值在usePrintable()
方法中没卵用
改进与简化如下:
public class PrintableDemo {
public static void main(String[] args) {
System.out.println(
// 省略了 return 语句 与 传入参数的括号
usePrintable(str -> str + "Printable.print() 方法覆写")
);
}
// usePrintable 返回了 Printable.print() 的返回值
private static String usePrintable(Printable printable) {
return printable.print("接口方法传参;\n");
}
}
双冒号运算符与方法引用简介
实例引入
在 Mybatis-plus 的 LambdaQueryWrapper中经常出现实体类双冒号运算符,它又是谁的缩写?
我们回看带参无返回值的情况:
@FunctionalInterface
interface Printable {
void print(String printStr);
}
public class PrintableDemo {
public static void main(String[] args) {
usePrintable((string) -> {
System.out.println(string);
});
}
private static void usePrintable(Printable printable) {
printable.print("接口方法传参;\n");
}
}
不免发现第8行只用了一个方法,并且Sout所传参数与print()方法所传参数完全一致,并且此时IDEA报黄“Lambda表达式可被替换为方法引用”:
替换后,main方法便只剩下了这一条简短的语句:
@FunctionalInterface
interface Printable {
void print(String printStr);
}
public class PrintableDemo {
public static void main(String[] args) {
usePrintable(System.out::println);
}
private static void usePrintable(Printable printable) {
printable.print("接口方法传参;\n");
}
}
我们可以认为在 usePrintable(System.out::println);
这条语句中,传入参数的意思是:System.out
静态类的方法 println
是函数式接口方法 print(String printStr)
的一种实现。
而Mybatis-plus的LambdaQueryWrapper情况就相对复杂了:
LambdaQueryWrapper<CartEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(CartEntity::getGoodsId);
探究 select()
方法:
// package com.baomidou.mybatisplus.core.conditions.query;
@SafeVarargs
public final LambdaQueryWrapper<T> select(SFunction<T, ?>... columns) {
return this.select(Arrays.asList(columns));
}
探究 SFunction<T, ?>
,发现其继承了Function<T, R>
函数式接口,这正是Java内置四大核心函数式接口之一:
// java 1.8 源码
// 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
// ....
}
我们可以认为在lambdaQueryWrapper.select(CartEntity::getGoodsId);
这条语句中,传入参数的意思是:CartEntity
类,其创建对象的实例方法 getGoodsId()
,就是 apply(T t)
的一种实现。
以上两例,都是将方法作为参数作为函数式接口方法的实现,我们称之为方法引用。
方法引用
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用
方法引用可以看作是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字指向一个方法
要求
实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致
格式
使用操作符“::”将类(或对象)与方法名分隔开来
三种使用情况
上述要求适用于下面的第一种和第二种情况
对象::实例方法名(又叫非静态方法)
类::静态方法名
类::实例方法名
注意:当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时,对应的是第三种情况,ClassName::methodName
参考:
【Java8新特性】Lambda表达式、函数式接口、方法引用、构造器引用 https://blog.csdn.net/guliguliguliguli/article/details/127251827