浅谈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() {}

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”。

面向对象思想强调“必须通过对象的形式来做事情”,函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做”;

从初步上理解,函数式编程要有两个特征:

  1. 函数是一等公民。即函数可以被赋值给变量,并且可以像普通的数据类型一样被存入对象,存入数组,存入其它的数据类型中。

对于这个特征,JavaScript 这语言就非常适合:

// javascript 匿名函数
const add = (a, b) => a + b;

// 函数作为“对象” 存入变量中
const compute = add; // 这里没有任何意义,在实际的开发中不推荐这么写

// 存入数组中
const list: any[] = [];
list.push(compute);
  1. 在函数式编程中,特别强调纯函数这个概念,即:相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,不会改变函数外变量的内容。

函数式编程目前支持的语言: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() 方法有几个很明显的特征:

  1. 方法没有传入参数

  2. 方法返回类型为 void

  3. 方法体中的内容是我们要做的内容

方式三:使用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表达式的代码分析

  1. ():里面没有内容,可以看成是方法形式参数为空

  2. ->:用箭头指向后面要做的事情

  3. { }:包含一段代码,我们称之为代码块,可以看成是方法体中的内容

在这个代码基础上,还能进一步进行优化,直接变成一行代码:

public class LambdaDemo {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Lambda启动了!")).start();
    }
}

综合上述三个方法可以看出,当我们使用实现类的方式实现需求的时候,我们可以看到我们需要先创建一个实体类,然后将实现类的对象传进去才可以使用。

当我们使用你们匿名内部类的方法实现需求时,我们可以发现需要重写 run 方法(当我们调用其他方法,忘记去重写什么方法时会比较懵逼),相比较也是比较麻烦的。

当我们使用Lambda表达式来实现需求时,我们可以看到我们不用关心创建了什么实体类、重写了什么方法。我们只需要关心它最终要做的事情是 System.out.println("多线程程序启动了...");

Lambda 表达式的标准形式

组成Lambda表达式的三要素:形式参数,箭头,代码块

Lambda表达式的格式

  1. 格式:(形式参数) -> {代码块}

  2. 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可;如果有一个参数,可以省略括号

  3. ->:由英文中画线和大于符号组成,固定写法。代表指向动作

  4. 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容

Lambda表达式的使用前提

  1. 有一个接口

  2. 接口中有且仅有一个抽象方法

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

参考:

  1. https://blog.csdn.net/qq_44096670/article/details/116495047

  2. 【Java8新特性】Lambda表达式、函数式接口、方法引用、构造器引用 https://blog.csdn.net/guliguliguliguli/article/details/127251827

  3. 知乎-函数式编程 https://zhuanlan.zhihu.com/p/271041158