Java函数式编程+Lambda表达式

文章目录

  • 函数式编程介绍
  • 纯函数
  • Lambda表达式基础
  • [函数式接口(Functional Interface)](#函数式接口(Functional Interface))
        • [1. **函数式接口的定义**](#1. 函数式接口的定义)
        • [2. **函数式接口与Lambda表达式的关系**](#2. 函数式接口与Lambda表达式的关系)
        • [3. **JDK内置的函数式接口**](#3. JDK内置的函数式接口)
        • [4. **Lambda表达式使用函数式接口的典型例子**](#4. Lambda表达式使用函数式接口的典型例子)
  • [**方法引用(Method Reference)详解**](#方法引用(Method Reference)详解)
    • [**1. 方法引用的基本概念**](#1. 方法引用的基本概念)
    • [**2. 方法引用的四种形式**](#2. 方法引用的四种形式)
      • [**2.1 引用静态方法**](#2.1 引用静态方法)
      • [**2.2 引用特定对象的实例方法**](#2.2 引用特定对象的实例方法)
      • [**2.3 引用构造方法**](#2.3 引用构造方法)
    • [**3. 方法引用与函数式接口的关系**](#3. 方法引用与函数式接口的关系)
    • [**4. 方法引用的实际应用**](#4. 方法引用的实际应用)
  • 常用的函数式接口

函数式编程介绍

Java8新引入函数式编程方式,大大的提高了编码效率。

从面向对象编程视角来看,程序中使用的变量,其实只是一个值的容器,这个容器中,可以放置不同的值。

从函数式编程视角,变量其实只是值的一个"别名",值本身是不能改的

采用函数式编程思想设计类,强调"类"所封装的数据,应该只被初始化一次,之后就不再更改。

JDK中的String类型,就是用"函数式编程"风格设计出来的一个例子。

Java 8引入了Lambda表达式特性,提供了Stream API,其内置的函数支持动态组合和级联调用,能够方便地实现"声明式"的编程方式。

函数式与面向对象编程的基本构造元素:

面向对象编程,编程的基本单元是"类",函数必须放在类中,从属于"类"。

函数式编程,以"函数"作为编程的基本构造块,函数之间可以相互协作和动态组合。

函数式编程能很容易地实现"行为的参数化"

函数封装了"行为",在函数式编程中,是"把函数作为值"来看待的。

既然"函数是值",那么它就可以作为另一个函数的参数或返回值,这个就是"行为的参数化"。

Java使用Lambda表达式来代表那些需要反复传递的行为,将其作为函数参数或返回值,从而实现了"行为参数化"。

这样讲有点抽象ヽ(´¬`)ノ,下面进行详细介绍。

纯函数

函数式编程中的"函数",强调要消除"副作用"。

所谓"副作用",就是指:

(1)函数的执行,受到其"运行上下文"的影响,在不同的运行环境中执行,会得到不同的结果。

(2)函数执行之后,会修改外界的数据,从而对外界的状态有所影响。

没有"副作用"的函数,可以放心地让多个线程调用。

抛出异常的函数,不满足"函数式编程"的要求

"纯函数"------没有副作用的函数:

一个方法是不是"Pure Function(纯函数)",关键就是它的运行,是不是有"副作用(Side Effect)"。纯函数是没有副作用的,只要输入参数值一定,它的结果总是一致的,从而可以安全地被跨线程调用而无需考虑线程同步问题。

java 复制代码
public class SideEffectIllustration {

    // 没有副作用的方法
    public int f1(int x) {
        return x * 2;
    }

    private int state = 0;
    // 有副作用的方法
    public int f2(int x) {
        state++;
        return x * 2 + state;
    }

    public static void main(String[] args) {
        SideEffectIllustration obj = new SideEffectIllustration();
        //创建10个线程,每个线程都调用f1或f2方法,观察多线程环境下
        //Pure Function的输出与有副作用的方法的输出有何区别
        Thread[] theads = new Thread[10];

        for (int i = 0; i < theads.length; i++) {
            final int index = i;
            theads[i] = new Thread(() -> {
                // Note:切换以下两句的注释,观察输出的结果
                System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f1(5)));
                //System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f2(5)));
            });
            theads[i].start();
        }
    }


}

在实际开发中,推荐尽量编写"纯函数"。

Lambda表达式基础

Lambda的引入

我们把只定义有一个抽象方法的接口,称为"单一抽象方法(SAM:Single Abstract Method)"的接口,在开发中可以有三种方式实现它:

传统方法

1. 顶层类
java 复制代码
interface MyInterface {
    void func();
}

class MyClass implements MyInterface {
    @Override
    public void func() {
        System.out.println("MyClass's func()");
    }
}

上述代码定义了一个MyInterface接口(它只定义了一个抽象方法,所以是SAM接口),接着,写了一个MyClass类实现这个接口,在这里,MyClass是一个顶层类。

然后,写了一个静态方法调用接口定义的方法:

java 复制代码
    public static void doWithMyInterface(MyInterface obj) {
        obj.func();
    }

使用传统编程方式,上面的代码是这样被调用的:

java 复制代码
        //传统方法,定义一个类实现接口,创建这个类的对象,
        //再把它传给doWithMyInterface()方法
        MyClass obj = new MyClass();
        doWithMyInterface(obj);
2. 内部类

除了使用顶层类,也可以使用内部类实现接口:

内部类的适用场景,主要是"仅在本类内部使用",不需要被外界调用,并且代码比较简短。

java 复制代码
        class MyInnerClass implements MyInterface{
            @Override
            public void func() {
                System.out.println("本地内部类,实现接口");
            }
        }
        //实例化本地内部类对象,传给示例方法
        doWithMyInterface(new MyInnerClass());
3. 匿名类

如果代码仅在特定方法内部调用,并且代码量也不大,可以直接使用匿名内部类实现接口:

java 复制代码
        doWithMyInterface(new MyInterface() {
            @Override
            public void func() {
                System.out.println("使用匿名内部类,实现接口");
            }
        });

Lambda

前面的代码是传统的经典的Java面向对象编程代码, 在开发中使用接口,所有代码可以很明确地分为"第一步、第二步、第三步......",很易于理解,也很规范,但每次都需要手工做这么多的事,似乎有点过于麻烦了。

为了便捷性考虑,Java设计者借鉴其他编程语言,把Lambda特性引入到了Java中。

就可以把上述的代码写成这样的形式。

java 复制代码
        //定义一个Lambda表达式,将其引用保存到变量中,
        //再把它传给doWithMyInterface()方法
        //从而可以节省下新定义一个类的工作任务
        MyInterface lambdaObj = () -> {
            System.out.println("Explicit Define Lambda object's func()");
        };
        doWithMyInterface(lambdaObj);

可以更进一步地简化为:

java 复制代码
        //直接把一个Lambda表达式作为doWithMyInterface()方法的参数
        //不仅不需要定义一个单独的类,甚至不再需要定义一个变量
        doWithMyInterface(() -> {
            System.out.println("inline lambda object's func()");
        });

** 直接执行Lambda表达式:**

要"执行"一个Lambda表达式所封装的代码,需要使用关联接口所定义的方法:

java 复制代码
        MyInterface lambdaObj2 = () -> {
            System.out.println("另一个Lambda表达式");
        };
        //Lambda表达式,也可以直接执行
        lambdaObj2.func();

在函数式编程代码中,函数与其它数据类型一样,也可以进行"赋值"和"传送",具体来说,就是可以定义"函数类型"的变量,函数可以成为另一个函数的"参数",函数也可以返回"另一个函数"。

可以把Lambda表达式理解为一种简洁的可传递匿名函数:它没有名称,但有参数列表,函数主体,返回类型,可能还有一个可以抛出的异常列表。

Lambda表达式格式:(如果方法体只有单行的话,可以把大括号去掉)

(参数列表) -> { 方法体 }

这是一些常见的Lambda使用例子:

再举一个完整的例子:

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class UseComparator {

    public static void main(String args[]) {

        List<String> strings = new ArrayList<String>();

        strings.add("CCC");
        strings.add("ddd");
        strings.add("EEE");
        strings.add("AAA");
        strings.add("bbb");
        //使用Lambda表达式重写上述代码段
        Comparator<String> comparator = (str1, str2) -> {
            return str1.compareToIgnoreCase(str2);
        };
        strings.sort(comparator);
        System.out.println("Sort with comparator");

        //输出排序结果
        for (String str : strings) {
            System.out.println(str);
        }
    }
}

我们可以用Lambda表达式来写comparator方法,并在sort中利用这个方法进行排序。

现在知道了如何编写Lambda表达式,但在哪里使用呢?可以通过函数式接口来使用Lambda表达式。

Java通过"函数式接口"+ Lambda表达式,实现函数式编程。

函数式接口(Functional Interface)

能接收一个Lambda表达式的变量,必须是接口类型,并且这种接口,还必须是一种"函数式接口(functional interface)"。

所谓"函数式接口",就是"只定义有一个抽象方法的接口",在Java 8之前,这种接口被称为"SAM:Single Abstract Method"接口。

Java 8中,使用"@FunctionalInterface"标识一个"函数式接口"。

函数式接口 是 Java 8 引入的一个概念,指 只有一个抽象方法 的接口。

它是 Lambda 表达式的基础,Lambda 表达式可以直接替代函数式接口的实现。


1. 函数式接口的定义

函数式接口在语法上与普通接口无异,但必须确保只有一个抽象方法。

示例:
java 复制代码
@FunctionalInterface
public interface MyFunction {
    int apply(int x, int y); // 唯一的抽象方法
}

注意:

  • Java 8 提供了 @FunctionalInterface 注解,用于显式声明一个接口为函数式接口。
  • 如果有多个抽象方法,编译器会报错。

即使不加 @FunctionalInterface,接口只有一个抽象方法时,依然可以作为函数式接口使用。


2. 函数式接口与Lambda表达式的关系

Lambda表达式的本质 是一种简化语法,用来表示函数式接口的实例。

当一个 Lambda 表达式被传递时,JVM 自动将其映射为对应函数式接口的实现。

关联逻辑:
  1. 函数式接口提供了唯一的抽象方法
  2. Lambda 表达式的代码实现对应函数式接口的唯一抽象方法。
示例:
java 复制代码
@FunctionalInterface
interface MyFunction {
    int apply(int x, int y);
}

// 使用Lambda表达式实现MyFunction接口
MyFunction add = (x, y) -> x + y;

System.out.println(add.apply(3, 5)); // 输出: 8

解释:

  • MyFunction 是一个函数式接口。
  • Lambda表达式 (x, y) -> x + y 实现了 apply 方法。

3. JDK内置的函数式接口

Java 提供了许多内置的函数式接口,位于 java.util.function 包中。这些接口可以直接配合 Lambda 表达式使用。

常见接口:
  1. Predicate<T>:接收一个参数,返回布尔值。

    java 复制代码
    Predicate<Integer> isEven = n -> n % 2 == 0;
    System.out.println(isEven.test(4)); // 输出: true
  2. Consumer<T>:接收一个参数,无返回值。

    java 复制代码
    Consumer<String> print = s -> System.out.println(s);
    print.accept("Hello, Lambda!"); // 输出: Hello, Lambda!
  3. Function<T, R>:接收一个参数,返回一个结果。

    java 复制代码
    Function<Integer, String> intToString = n -> "Number: " + n;
    System.out.println(intToString.apply(10)); // 输出: Number: 10
  4. Supplier<T>:无参数,返回一个结果。

    java 复制代码
    Supplier<Double> random = () -> Math.random();
    System.out.println(random.get()); // 输出: 随机数
  5. BiFunction<T, U, R>:接收两个参数,返回一个结果。

    java 复制代码
    BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
    System.out.println(multiply.apply(3, 4)); // 输出: 12

4. Lambda表达式使用函数式接口的典型例子
  1. 线程启动:使用 Runnable 接口

    java 复制代码
    // 传统写法
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Thread running...");
        }
    }).start();
    
    // 使用Lambda
    new Thread(() -> System.out.println("Thread running...")).start();
  2. 集合操作:Comparator接口

    java 复制代码
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
    // 使用Lambda表达式简化排序
    names.sort((a, b) -> a.compareTo(b));
    System.out.println(names);
  3. 事件处理:ActionListener接口

    java 复制代码
    JButton button = new JButton("Click Me");
    button.addActionListener(e -> System.out.println("Button clicked!"));

方法引用(Method Reference)详解

方法引用 是 Java 8 引入的一种简化 Lambda 表达式的语法,允许开发者通过直接引用已有方法来实现函数式接口的抽象方法,从而使代码更简洁、更可读。

Lambda表达式可以进一步简化为方法引用,直接使用现有方法实现接口的抽象方法。

每个方法引用都存在一个等效的 lambda 表达式

1. 方法引用的基本概念

  • 方法引用是 Lambda 表达式的简化形式。
  • 它使用双冒号 :: 操作符来引用方法。
  • 适用于 Lambda 表达式仅调用一个已有方法的场景。
方法引用的语法结构
java 复制代码
ClassName::methodName

例如:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);

// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;

在这两个例子中,Integer::parseInts -> Integer.parseInt(s) 的简化形式。


2. 方法引用的四种形式

2.1 引用静态方法

适用于 Lambda 表达式调用某个类的静态方法。

语法:

java 复制代码
ClassName::staticMethod

示例:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);

// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;

System.out.println(methodRef.apply("123")); // 输出: 123

分析:

  • Lambda 表达式 s -> Integer.parseInt(s) 调用的是 Integer 类的静态方法 parseInt
  • 通过 Integer::parseInt 简化了代码。

2.2 引用特定对象的实例方法

适用于 Lambda 表达式调用特定对象的实例方法。

语法:

java 复制代码
instance::instanceMethod

示例1:

java 复制代码
// 特定对象
String str = "Hello";

// Lambda 表达式
Supplier<Integer> lambda = () -> str.length();

// 方法引用
Supplier<Integer> methodRef = str::length;

System.out.println(methodRef.get()); // 输出: 5

分析:

  • Lambda 表达式 () -> str.length() 调用的是 str 对象的 length 方法。
  • 通过 str::length 简化代码。

示例2:

java 复制代码
// Lambda 表达式
BiFunction<String, String, Boolean> lambda = (s1, s2) -> s1.equals(s2);

// 方法引用
BiFunction<String, String, Boolean> methodRef = String::equals;

System.out.println(methodRef.apply("abc", "abc")); // 输出: true
System.out.println(methodRef.apply("abc", "def")); // 输出: false

分析:

  • Lambda 表达式 (s1, s2) -> s1.equals(s2) 调用的是 String 类实例的 equals 方法。
  • 通过 String::equals 进一步简化。

2.3 引用构造方法

适用于 Lambda 表达式用于创建对象的场景。

语法:

java 复制代码
ClassName::new

示例:

java 复制代码
// Lambda 表达式
Supplier<List<String>> lambda = () -> new ArrayList<>();

// 方法引用
Supplier<List<String>> methodRef = ArrayList::new;

List<String> list = methodRef.get();
System.out.println(list); // 输出: []

带参数的构造方法引用:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> new Integer(s);

// 方法引用
Function<String, Integer> methodRef = Integer::new;

System.out.println(methodRef.apply("123")); // 输出: 123

3. 方法引用与函数式接口的关系

方法引用本质上是对函数式接口的实现。

  • 函数式接口 要求实现唯一的抽象方法。
  • 方法引用 的方法与该抽象方法的签名必须一致。

示例:

java 复制代码
@FunctionalInterface
interface MyFunction {
    void print(String s);
}

// 使用 Lambda 表达式
MyFunction lambda = s -> System.out.println(s);

// 使用方法引用
MyFunction methodRef = System.out::println;

lambda.print("Hello, Lambda!");   // 输出: Hello, Lambda!
methodRef.print("Hello, Method Reference!"); // 输出: Hello, Method Reference!

4. 方法引用的实际应用

  1. 集合排序
java 复制代码
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

// 使用 Lambda 表达式
names.sort((a, b) -> a.compareTo(b));

// 使用方法引用
names.sort(String::compareTo);

System.out.println(names); // 输出: [Alice, Bob, Charlie]
  1. 流处理(Stream API)
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 使用 Lambda 表达式
names.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));

// 使用方法引用
names.stream().map(String::toUpperCase).forEach(System.out::println);

常用的函数式接口

1. Consumer接口(级联多个Consumer)

Consumer的基本用法

Consumer<T> 接口用于接收一个输入参数并对其执行某些操作,但不会返回结果。常见用法是打印日志、更新状态、操作集合元素等。

  • 核心方法:

    java 复制代码
    void accept(T t);
    • 接收一个参数 t,对其执行操作。
    • 没有返回值。
级联Consumer - andThen方法

Consumer 提供了默认方法 andThen,可以将多个 Consumer 串联起来,按照顺序依次执行每个 Consumer 的逻辑。

  • 方法定义:

    java 复制代码
    default Consumer<T> andThen(Consumer<? super T> after)
    • 参数 after: 另一个 Consumer,会在当前 Consumer 执行完之后被调用。
    • 返回一个新的 Consumer,依次执行两个 Consumer 的操作。
  • 示例代码:

    java 复制代码
    public static void main(String[] args) {
        Consumer<String> consumer1 = str -> System.out.println("Consumer 1: " + str);
        Consumer<String> consumer2 = str -> System.out.println("Consumer 2: " + str.toUpperCase());
    
        // 将两个Consumer级联
        Consumer<String> combinedConsumer = consumer1.andThen(consumer2);
    
        combinedConsumer.accept("hello");
    }
    • 输出:

      复制代码
      Consumer 1: hello
      Consumer 2: HELLO

更复杂的级联示例

可以串联多个 Consumer 来实现更复杂的操作。

java 复制代码
public static void main(String[] args) {
    Consumer<String> consumer1 = str -> System.out.println("Step 1: " + str.trim());
    Consumer<String> consumer2 = str -> System.out.println("Step 2: " + str.toLowerCase());
    Consumer<String> consumer3 = str -> System.out.println("Step 3: " + str.toUpperCase());

    // 级联三个Consumer
    Consumer<String> combinedConsumer = consumer1.andThen(consumer2).andThen(consumer3);

    combinedConsumer.accept("  HeLLo WoRLd  ");
}
  • 输出:

    复制代码
    Step 1: HeLLo WoRLd
    Step 2: hello world
    Step 3: HELLO WORLD

2. Predicate接口与对象查找

Predicate的基本概念

Predicate<T> 接口主要用于定义一个"判断规则",用于表示一个"布尔值"函数。它接收一个输入参数,并返回一个布尔值,用于条件判断。

  • 核心方法:

    java 复制代码
    boolean test(T t);
    • 参数 t: 输入值。
    • 返回值:布尔值,用于判断输入是否满足条件。
  • 默认方法:

    • and : 将多个 Predicate 串联,所有条件均为 true 时返回 true
    • or : 至少一个条件为 true 时返回 true
    • negate : 对当前 Predicate 结果取反。
    • isEqual: 检查对象是否相等。

对象查找中的应用

Predicate 通常用于过滤集合、查找符合条件的对象。

  • 示例代码(查找满足条件的对象):

    java 复制代码
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie", "David");
    
        // 定义Predicate,查找长度大于3的名字
        Predicate<String> lengthPredicate = name -> name.length() > 3;
    
        // 查找符合条件的名字
        names.stream()
             .filter(lengthPredicate)
             .forEach(System.out::println);
    }
    • 输出:

      复制代码
      Alice
      Charlie
      David
  • 级联使用多个Predicate:

  • java 复制代码
    Predicate<String> startsWithA = name -> name.startsWith("A");
    Predicate<String> lengthPredicate = name -> name.length() > 3;
    
    // 组合条件:以A开头且长度大于3
    Predicate<String> combinedPredicate = startsWithA.and(lengthPredicate);
    
    names.stream()
         .filter(combinedPredicate)
         .forEach(System.out::println);
    • 输出:

      复制代码
      Alice

3. Function接口及相关接口

Function的基本概念

Function<T, R> 是 Java 8 中的函数式接口,此接口定义了一个apply方法,它接收一个T类型的对象,返回一个R类型的对象:

  • 核心方法:

    java 复制代码
    R apply(T t);
    • 参数 t: 输入值。
    • 返回值:类型为 R 的结果。
  • 默认方法:

    • andThen : 先执行当前 Function,再将结果传给另一个 Function
    • compose : 先执行参数指定的 Function,再将结果传递给当前 Function

示例代码
  • 基本用法:

    java 复制代码
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();
    
        System.out.println(stringLength.apply("Hello")); // 输出 5
    }
  • 使用 andThencompose:

    java 复制代码
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();
        Function<Integer, Integer> square = num -> num * num;
    
        // andThen: 先计算长度,再平方
        System.out.println(stringLength.andThen(square).apply("Hello")); // 输出 25
    
        // compose: 先平方长度,再计算平方
        System.out.println(square.compose(stringLength).apply("Hello")); // 输出 25
    }

相关接口
  1. BiFunction<T, U, R>

    作为Function接口的特例,有一个BiFunction<T, U, R>接口,它所定义的apply方法接收两个参数:T和U类型的,然后返回一个R类型的对象。

    • 表示一个接收两个参数的函数,返回一个结果。

    • 核心方法:

      java 复制代码
      R apply(T t, U u);
    • 示例:

      java 复制代码
      BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
      System.out.println(add.apply(5, 10)); // 输出 15
  2. UnaryOperator

    • Function<T, T> 的子接口,表示输入和输出类型相同的函数。其实就是Function<T,T>的简写。

    • 示例:

      java 复制代码
      UnaryOperator<Integer> square = x -> x * x;
      System.out.println(square.apply(5)); // 输出 25
  3. BinaryOperator

    • BiFunction<T, T, T> 的子接口,等价于BiFunction<T,T,T>,表示两个相同类型参数的函数,并返回相同类型的结果。

    • 示例:

      java 复制代码
      BinaryOperator<Integer> multiply = (a, b) -> a * b;
      System.out.println(multiply.apply(2, 3)); // 输出 6
  4. IntToDoubleFunction

    • 表示接收一个 int 类型参数并返回一个 double 类型结果的函数。

    • 示例:

      java 复制代码
      IntToDoubleFunction half = x -> x / 2.0;
      System.out.println(half.applyAsDouble(10)); // 输出 5.0
相关推荐
腥臭腐朽的日子熠熠生辉32 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian34 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之39 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen1 小时前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
松韬2 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
绝顶少年2 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端