• Lambda表达式&Stream流-函数式编程-java8函数式编程(Lambda表达式,Optional,Stream流)从入门到精通-最通俗易懂


    一、函数式编程

    1.1  概念

            面向对象思想需要关注用什么对象完成什么事情。而函数式编程思想就类似于我们数学中的函数。它主要关注的是对数据进行了什么操作。

            函数式编程是一种编程范式,它将计算视为函数的运算过程。函数式编程强调将程序分解成独立的、无状态的函数,避免使用可变的状态和共享的数据。函数式编程主要基于数学中的Lambda演算理论和一些数学函数的概念。

    函数式编程具有以下特点:
            1. 纯函数:函数式编程中的函数是纯函数,即给定相同的输入,总是产生相同的输出,不会对外部状态产生任何影响。
            2. 不可变数据:函数式编程中的数据一旦创建就不会被修改,任何改变都是通过创建新的数据来实现。
            3. 高阶函数:函数可以作为参数传递给其他函数,也可以作为返回值返回。
            4. 递归:函数式编程中常使用递归来进行迭代和重复的操作。
            5. 引用透明:函数式编程中的函数调用可以被视为对表达式的求值,因此引用透明性是一个重要的特点,即可以替换函数调用的结果而不影响程序的行为。

    1.2  优点

            1.代码简洁,开发快速

            2.接近自然语言,易于理解

            3.易于"并发编程"

    二、Lambda表达式

    2.1  概念

    • lambda是JDK8中的一个语法糖,可以对某些匿名内部类的写法进行优化,让函数式编程只关注数据而不是对象。
    • 基本格式:(参数列表)->{代码}

    Lambda可以对某些匿名内部类的写法进行简化,Lambda表达式只支持函数式接口。也就是只有一个抽象方法的接口。因此,可以对某些匿名内部类进行简化的条件是:

            1.是一个接口的匿名内部类

            2.这个接口中只有一个抽象方法。

            当满足这两个条件,就可以使用Lambda对这个匿名内部类进行简化。

     Lambda表达式是一种匿名函数,它可以简化对于单一方法接口(Single Abstract Method,SAM)的实现。

    2.2  语法

            (参数列表)->{代码}

    2.3  案例

    2.3.1  案例一

    匿名内部类的写法

    1. public static void main(String[] args) {
    2. new Thread(new Runnable() {
    3. @Override
    4. public void run() {
    5. System.out.println("新线程的run()执行了!");
    6. }
    7. }).start();
    8. }

    以上是还未被Lambda优化的一个匿名内部类,当使用Lambda优化过后,如下:

    1. public static void main(String[] args) {
    2. new Thread(() ->{
    3. System.out.println("新线程的run()执行了2!");
    4. }).start();
    5. }

    2.3.2  案例二

    匿名内部类的写法

    1. public class LambdaTest {
    2. public static void main(String[] args) {
    3. //对printNum()方法进行调用
    4. LambdaTest.printNum(new IntPredicate() { //对接口的实现
    5. @Override
    6. public boolean test(int value) {
    7. return value%2==0;
    8. }
    9. });
    10. }
    11. //自定义一个printNum()方法
    12. public static void printNum(IntPredicate predicate){
    13. int[] arr={1,2,3,4,5,6,7,8,9,10};
    14. for (int i:arr){
    15. if(predicate.test(i)){
    16. System.out.println(i);
    17. }
    18. }
    19. }
    20. }

     采用Lambda表达式的

    1. public class LambdaTest {
    2. public static void main(String[] args) {
    3. LambdaTest.printNum((int value)-> {
    4. return value%2==0;
    5. }
    6. );
    7. }
    8. public static void printNum(IntPredicate predicate){
    9. int[] arr={1,2,3,4,5,6,7,8,9,10};
    10. for (int i:arr){
    11. if(predicate.test(i)){
    12. System.out.println(i);
    13. }
    14. }
    15. }
    16. }

    2.4  省略规则

            1.参数类型可以省略

            2.方法体只有一句代码时大括号{ }、return关键字和唯一语句代码的分号可以省略

            3.方法只有一个参数时小括号可以省略

            (以上这些规则都记不住也可以省略不记)

    省略案例:

    接口

    1. package com.java.lambda;
    2. @FunctionalInterface // 一旦加上这个注解必须是函数式接口,里面只能有一个抽象方法
    3. public interface Swimming {
    4. void swim(String name);
    5. }

    启动类 

    1. package com.java.lambda;
    2. // 一个参数,无返回值
    3. public class LambdaDemo02 {
    4. public static void main(String[] args) {
    5. // 完整写法
    6. go((String s) -> {
    7. System.out.println(s + "游泳贼厉害。。。");
    8. });
    9. System.out.println("-------------");
    10. // 省略写法
    11. go(s -> System.out.println(s + "游泳贼厉害。。。"));
    12. }
    13. private static void go(Swimming swimming) {
    14. swimming.swim("小红");
    15. }
    16. }

    三、Stream流

    3.1  概述

            流是Java 8 API添加的一个新的抽象,称为流Stream,以一种声明性方式处理数据集合(侧重对于源数据计算能力的封装,并且支持序列与并行两种操作方式)

            Stream流是从支持数据处理操作的源生成的元素序列,源可以是数组、文件、集合、函数。流不是集合元素,它不是数据结构并不保存数据,它的主要目的在于计算

            Stream流是对集合(Collection)对象功能的增强,它可以被用来对集合或数组进行链状流式的操作。与Lambda表达式结合,可以提高编程效率、间接性和程序可读性。

    3.2  创建流

    (1)单列集合:集合对象.stream()

    1. List<String> list = Arrays.asList("apple", "banana", "orange");
    2. Stream<String> stream = list.stream();

    (2)数组:Arrays .stream(数组)或者使用Stream.of 来创建

    1. Integer[] arr = {1,2,3,4,5};
    2. stream<Integer> stream = Arrays.stream(arr);
    3. stream<Integer> stream2 = Stream.of (arr);

    (3)  双列集合:转换成单列集合后再创建

    1. Map<string, Integer> map = new HashMap<>();
    2. map.put("蜡笔小新",19);
    3. map.put("黑子",17);
    4. map.put("日向翔阳",16);
    5. Stream<Map.Entry<String,Integer>> stream = map.entryset().stream() ;

    3.3  操作符

    流的操作类型主要分为两种:中间操作符、终端操作符

    3.4  中间操作符

    流方法                 含义                                                示例
    filter用于通过设置的条件过滤出元素

    List strings = Arrays.asList(“abc”, “”, “bc”, “efg”, “abcd”,"", “jkl”);

    List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

    map接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)

    List strings = Arrays.asList(“abc”, “abc”, “bc”, “efg”, “abcd”,“jkl”, “jkl”);

    List mapped = strings.stream().map(str->str+"-IT").collect(Collectors.toList());

    distinct返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流List numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
    sorted返回排序后的流

    List strings1 = Arrays.asList(“abc”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    List sorted1 = strings1.stream().sorted().collect(Collectors.toList());

    limit会返回一个不超过给定长度的流

    List strings = Arrays.asList(“abc”, “abc”, “bc”, “efg”, “abcd”,“jkl”, “jkl”);

    List limited = strings.stream().limit(3).collect(Collectors.toList());

    skip返回一个扔掉了前n个元素的流

    List strings = Arrays.asList(“abc”, “abc”, “bc”, “efg”, “abcd”,“jkl”, “jkl”);

    List skiped = strings.stream().skip(3).collect(Collectors.toList());

    flatMap使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流

    List strings = Arrays.asList(“abc”, “abc”, “bc”, “efg”, “abcd”,“jkl”, “jkl”);

    Stream flatMap = strings.stream().flatMap(Java8StreamTest::getCharacterByString);

    peek对元素进行遍历处理 

    List strings = Arrays.asList(“abc”, “abc”, “bc”, “efg”, “abcd”,“jkl”, “jkl”);

    strings .stream().peek(str-> str + "a").forEach(System.out::println);

    (1)filter

            可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

    (2)map

            可以把对流中的元素进行计算或转换。

    (3)distinct

            可以去除流中的重复元素

    注意: distinct方法是依赖object的equals方法来判断是否是相同对象的。所以需要注意重写equals方法。

    (4)sorted

            可以对流中的元素进行排序

    注意:如果调用空参的sorted()方法,需要流中的元素是实现了Comparable。

    (5)limit

            可以设置流的最大长度,超出的部分将被抛弃。

    (6)skip

            跳过流中的前n个元素,返回剩下的元素

    (7)flatMap

            map只能把一个对象转换成另一个对象来作为流中的元素。而flatMap可以把一个对象转换成多个对象作为流中的元素。(简而言之,可以直接将List对象转换为流对象)

    ​​​​​​​map将流中的每个元素都映射为一个新的元素;flatmap将流中的每个元素转换为一个流,然后将这些流合并成一个新的流。

    3.5  终结操作符

    流方法                 含义                                                示例
    collect收集器,将流转换为其他形式

    List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    Set set = strings.stream().collect(Collectors.toSet());

    List list = strings.stream().collect(Collectors.toList());

    Map map = strings.stream().collect(Collectors.toMap(v ->v.concat("_name"), v1 -> v1, (v1, v2) -> v1));

    forEach遍历流List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);strings.stream().forEach(s -> out.println(s));
    findFirst返回第一个元素

    List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    Optional first = strings.stream().findFirst();

    findAny将返回当前流中的任意元素

    List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    Optional any = strings.stream().findAny();

    count返回流中元素总数

    List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    long count = strings.stream().count();

    sum求和int sum = userList.stream().mapToInt(User::getId).sum();
    max最大值int max = userList.stream().max(Comparator.comparingInt(User::getId)).get().getId();
    min最小值int min = userList.stream().min(Comparator.comparingInt(User::getId)).get().getId();
    anyMatch检查是否至少匹配一个元素,返回boolean

    List strings = Arrays.asList(“abc”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    boolean b = strings.stream().anyMatch(s -> s == “abc”);

    allMatch检查是否匹配所有元素,返回boolean

    List strings = Arrays.asList(“abc”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    boolean b = strings.stream().allMatch(s -> s == “abc”);

    noneMatch检查是否没有匹配所有元素,返回boolean

    List strings = Arrays.asList(“abc”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    boolean b = strings.stream().noneMatch(s -> s == “abc”);

    reduce可以将流中元素反复结合起来,得到一个值

    List strings = Arrays.asList(“cv”, “abd”, “aba”, “efg”, “abcd”,“jkl”, “jkl”);

    Optional reduce = strings.stream().reduce((acc,item) -> {return acc+item;});if(reduce.isPresent())out.println(reduce.get());

    (1)forEach

            对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作。

    (2)count

            可以用来获取当前流中元素的个数。

    (3)max&min

            可以用来或者流中的最值。

    (4)collect

            把当前流转换成一个集合。

    (5)匹配-anyMatch

            可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型。

    (6)匹配-allMatch

            可以用来判断是否都符合匹配条件,结果为boolean类型。如果都符合结果为true,否则结果为false。

    (7)匹配-noneMatch

            可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为true,否则结果为false

    (8)查找-findAny

            获取流中的任意一个元素。该方法没有办法保证获取的一定是流中的第一个元素。

    (9)查找-findFirst

            获取流中的第一个元素。

    (10)reduce归并

            对流中的数据按照你指定的计算方式计算出一个结果。(缩减操作)
            reduce的作用是把stream中的元素遍历后给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算。

    该计算方式为两个参数的重载形式的内部计算方式。 


    reduce—个参数的重载形式内部的计算:

    3.6  Stream流的注意事项

    1.惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
    2.流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
    3.不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)

    四、Optional

    4.1  简介

      java.util.Optional类是Java8为了解决null值判断问题,借鉴google guava类库的Optional类而引入的一个同名Optional类。java.util.Optional类可以包含或不包含null值的容器对象。如果存在值,则isPresent方法将返回true,而get方法将返回该值。

      Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在,原来用null值表示一个值不存在,现在用Optional可以更好的表达这个概念,并且可以避免空指针异常

    4.2  Optional容器类的常用方法

    方法用途
    Optional of(T value)创建一个Optional 实例
    Optional empty()创建一个空的Optional 实例
    Optional ofNullable(T value)若value不为null,则创建一个Optional 实例,否则 创建一个空实例
    boolean isPresent()判断是否包含值 ,如果存在值,则isPresent方法将返回true
    ifPresent(Consumer consumer)如果包装对象的值非空,运行Consumer对象的accept()方法
    Optional filter(Predicate predicate)如果符合Predicate的条件,返回Optional对象本身,否则返回一个空的Optional对象。
    Optional map(Function mapper)如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
    Optional flatMap(Function> mapper)与map类似,要求返回值必须是Optional
    T orElse(T other)如果调用对象包含值,则返回该值,否则返回ohther
    T orElseGet(Supplier other)如果调用对象包含值,则返回该值,否则返回ohther 获取的值

    4.3  Optional 对象的创建

    1、static Optional empty():该方法返回一个value为空的Optional对象

    2、static Optional of(T value):该方法用于指定value创建一个Optional对象,value不能为null,否则抛出异常

    3、static Optional ofNullable(T value):该方法和of()方法的作用一样,但value允许为null
     

    1.使用 of 创建

    1. Student student = new Student("王五", 80);
    2. Optional<Student> optional = Optional.of(student);

    2.使用 ofNullable 创建

    1. Student student = new Student("王五", 80);
    2. Optional<Student> optional = Optional.ofNullable(student);

    3.of 和 ofNullable 的区别

    of 接收的值不能为 null,否则会报空指针异常

    1. Student student = null;
    2. //of 里只要传的参数只要是 null,就会报空指针异常
    3. Optional<Student> optional = Optional.of(student);

    ofNullable 接收的值可以是 null,不会报空指针异常,但如果接收的值是是 null,在使用 get() 获取的时候就会报空指针:

    1. Student student = null;
    2. //ofNullable 接收的值可以为 null
    3. Optional<Student> optional = Optional.ofNullable(student);
    4. //如果上面ofNullable 里接收的值 为 null ,下面使用 get() 获取对象会报空指针异常
    5. Student student1 = optional.get();

    4.4  判断

    isPresent()方法

    isPresent()方法用于判断value是否存在,不为NULL则返回true,如果为NULL则返回false

    1. public boolean isPresent() {
    2. return value != null;
    3. }

    使用案例

    1. @Test
    2. public void testIsPresent() {
    3. Optional<String> optional = Optional.of("thinkwon");
    4. Optional<String> optional1 = Optional.ofNullable(null);
    5. System.out.println(optional.isPresent());
    6. System.out.println(optional1.isPresent());
    7. }

    输出结果

    1. true
    2. false

    4.5  安全的消费值

    我们获取到一个Optional对象后肯定需要对其中的数据进行使用。这时候我们可以使用其ifPresent方法对来消费其中的值。这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码。这样使用起来就更加安全了。

    ifPresent(Consumer consumer)方法

    ifPresent()方法接受一个Consumer对象(消费函数),如果包装对象的值非空,运行Consumer对象的accept()方法,进行安全消费

    1. public void ifPresent(Consumersuper T> consumer) {
    2. if (value != null)
    3. consumer.accept(value);
    4. }

    使用案例

    1. @Test
    2. public void testIfPresent() {
    3. Optional<String> optional = Optional.of("thinkwon");
    4. optional.ifPresent(s -> System.out.println("the String is " + s));
    5. }

    输出结果

    the String is thinkwon
    

    4.6  安全的获取值

    1.get()方法

    get()方法主要用于返回包装对象的实际值,但是如果包装对象值为null,会抛出NoSuchElementException异常(不安全

    1. public T get() {
    2. if (value == null) {
    3. throw new NoSuchElementException("No value present");
    4. }
    5. return value;
    6. }

    使用案例

    1. @Test
    2. public void testGet() {
    3. Optional<String> optional = Optional.of("thinkwon");
    4. Optional<String> optional1 = Optional.ofNullable(null);
    5. System.out.println(optional.get());
    6. System.out.println(optional1.get());
    7. }

    输出结果

    1. thinkwon
    2. java.util.NoSuchElementException: No value present

    2.orElseGet()方法

    获取数据并且设置数据为空时的默认值。如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建对象作为默认值返回。(安全的获取值)

    1. public T orElseGet(Supplierextends T> supplier) {
    2. return value != null ? value : supplier.get();
    3. }

    使用案例

    1. @Test
    2. public void testOrElseGet() {
    3. String unkown = (String) Optional.ofNullable(null).orElseGet(() -> "unkown");
    4. System.out.println(unkown);
    5. }

    输出结果

    unkown
    

    3.orElseThrow()方法

    orElseThrow()方法其实与orElseGet()方法非常相似了,入参都是Supplier对象,只不过orElseThrow()Supplier对象必须返回一个Throwable异常,并在orElseThrow()中将异常抛出,orElseThrow()方法适用于包装对象值为空时需要抛出特定异常的场景。

    1. public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    2. if (value != null) {
    3. return value;
    4. } else {
    5. throw exceptionSupplier.get();
    6. }
    7. }

    使用案例

    1. @Test
    2. public void testOrElseThrow() {
    3. Optional.ofNullable(null).orElseThrow(() -> new RuntimeException("unkown"));
    4. }

    输出结果

    java.lang.RuntimeException: unkown
    

    4.7  过滤数据

    filter()方法

    filter()方法接受参数为Predicate对象,用于对Optional对象进行过滤,如果符合Predicate的条件,返回Optional对象本身,否则返回一个空的Optional对象。

    1. public Optional filter(Predicatesuper T> predicate) {
    2. Objects.requireNonNull(predicate);
    3. if (!isPresent()) {
    4. return this;
    5. } else {
    6. return predicate.test(value) ? this : empty();
    7. }
    8. }

    使用案例

    1. @Test
    2. public void testFilter() {
    3. Optional.of("thinkwon").filter(s -> s.length() > 2)
    4. .ifPresent(s -> System.out.println("The length of String is greater than 2 and String is " + s));
    5. }

    输出结果

    The length of String is greater than 2 and String is thinkwon
    

    4.8  数据转换

    Optional还提供了map可以让我们的对数据进行转换,并且转换得到的数据也还是被Optional包装好的,保证了我们的使用安全。例如我们想获取作家的书籍集合。
     

    五、函数式接口

    5.1  概述

            只有一个抽象方法接口我们称之为函数接口
            JDK的函数式接口都加上了@Functionallnterface注解进行标识。但是无论是否加上该注解只要接口中只有一个抽象方法,都是函数式接口。


    我们来看一个在之前我们就经常使用的Runnable接口,Runnable接口就是一个函数式接口,下面的截图为 Java 源码:

    我们看到Runnable接口中只包含一个抽象的run()方法,并且在接口上标注了一个@FuncationInterface注解,此注解就是 Java 8 新增的注解,用来标识一个函数式接口。

    5.2  常见函数式接口

    1.  Consumer消费接口

    2.  Function计算转换接口

    3.  Predicate判断接口

    4.  Supplier生产型接口

    六、方法引用

            我们在使用lambda时,如果方法体中只有一个方法的调用的话(包括构造方法),我们可以用方法引用进一步简化代码。

    6.1  推荐用法

            我们在使用lambda时不需要考虑什么时候用方法引用,用哪种方法引用,方法引用的格式是什么。我们只需要在写完lambda方法发现方法体只有一行代码,并且是方法的调用时使用快捷键尝试是否能够转换成方法引用即可。
            当我们方法引用使用的多了慢慢的也可以直接写出方法引用。
     

    6.2基本格式

            类名或者对象名 :: 方法名


     

    6.3  语法详解(了解)

    6.3.1引用类的静态方法

    格式:类名::方法名

    使用前提:
            如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个静态方法中,这个时候我们就可以引用类的静态方法。

    6.3.2引用对象的实例方法

    格式: 对象名::方法名

    使用前提:
            如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用对象的实例方法

    6.3.3引用类的实例方法

    格式:类名::方法名

    使用前提:
            如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。


    6.3.4  构造器引用

            如果方法体中的一行代码是构造器的话就可以使用构造器引用。

    格式:  类名::new

    使用前提:
            如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们把要重写的抽象方法中的所有的参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用构造器。

    七、高级用法

    7.1  基本数据类型优化

            我们之前用到的很多Stream的方法由于都使用了泛型。所以涉及到的参数和返回值都是引用数据类型。

            即使我们操作的是整数小数,但是实际用的都是他们的包装类。JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便。但是你一定要知道装箱和拆箱肯定是要消耗时间的。虽然这个时间消耗很下。但是在大量的数据不断的重复装箱拆箱的时候,你就不能无视这个时间损耗了。

            所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方法。

            例如: mapTolnt,mapToLong,mapToDouble,flatMapTolnt,flatMaplODounle等。

    7.2  并行流

            当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完全。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识。而如果我们使用Stream的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率。

    parallel() 方法可以把串行流转换成并行流。

    也可以通过parallelStream() 方法直接获取并行流对象。

  • 相关阅读:
    【soc】— spl&&uboot校验方法
    array详解
    SAP 特殊采购类遇到Q库存
    [附源码]JAVA毕业设计基于web的面向公众的食品安全知识系统(系统+LW)
    php中$this->的解释
    JSP session对象
    P1875 佳佳的魔法药水
    【123. 买卖股票的最佳时机 III】
    Rest Template 使用
    [数据结构+算法]关于动态规划dp入门--01背包问题
  • 原文地址:https://blog.csdn.net/weixin_55772633/article/details/134269916