SpringBoot2.0不容错过的新特性 WebFlux响应式编程-学习笔记(一)

整理自慕课网课程:https://coding.imooc.com/class/209.html

第一节 课程介绍

学习之路

第二节 函数式编程和lambda表达式

一、函数式编程和lambda表达式

1. 什么是函数式编程

函数式编程是一种相对于命令式编程的一种编程范式,它不是一种具体的技术,而是一种。能够熟练使用流 API & lambda表达式 & 流相关思想,就可以说自己会函数式编程了。

2. 为什么要使用函数式编程

能让我们以一种更加优雅的方式进行编程。

  • 函数式编程与命令式编程相比
    1)不同点:
    关注点不同,命令式编程关注,函数式编程关注(我要实现什么样的功能而不用管实现的细节)
    2)优点:
    可以使代码更加的简短,更加的好读。

3. lambda表达式初接触

示例: 求数组中的最大值,如果数据量太大,想要处理更高效,jdk8以前,只能自己创建线程池,自己拆分,而jdk8以后只需要加上parallel(),意思就是告诉它我要多线程的处理该数据,以此可以看到他的魅力(体验命令式编程与函数式编程的区别)

public class MinDemo
{
    public static void main(String[] args) {
        int[] nums = {33,55,-55,90,-666,90};

        int min = Integer.MAX_VALUE;
        for (int i : nums) {
            if(i < min) {
                min = i;
            }
        }
        System.out.println(min);

        //jdk8 lambda,parallel()多线程处理
        int min2 = IntStream.of(nums).parallel().min().getAsInt();
        System.out.println(min2);
    }
}

4. Lambda表达式的本质是一个

写法为:函数的参数 -> 函数的执行体
如:s -> System.out.println(s);

public class ThreadDemo
{
    public static void main(String[] args)
    {
        Object target = new Runnable()
        {
            @Override
            public void run()
            {
                System.out.println("hello");
            }
        };
        new Thread((Runnable) target).start();

        //jdk8 lambda
        Object target2 = (Runnable) () -> System.out.println("shasha");
        Runnable target3 = () -> System.out.println("shasha"); //false
        System.out.println(target2 == target3);

        new Thread((Runnable) target2).start();
    }
}
  • Lambda表达式返回的是一个。接口里一个要实现的方法(要实现的方法并不是指接口里只能有一个方法)。
  • Lambda表达式,故不需要接口也可以。
  • jdk8接口新特性
    1)接口里只有一个要实现的方法,单一责任制
    2)新增默认方法,即,指的是接口默认实现的方法(如果一个接口继承了其他多个接口,default默认会冲突,指定用哪个父类的default即可)
    3)引入函数接口,如注解@FunctionalInterface,标明此接口为函数接口,该接口只能有一个待实现的方法。
@FunctionalInterface
interface Interface1 {
    //只有这一个待实现的接口
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }

    static int sub(int x, int y) {
        return x - y;
    }
}

@FunctionalInterface
interface Interface2 {
    //只有这一个待实现的接口
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

interface Interface3 extends Interface1, Interface2 {
    @Override
    default int add(int x, int y) {
        return Interface1.super.add(x, y);
    }
}

public class LambdaDemo1 {
    public static void main(String[] args) {
        Interface1 i1 = (i) -> i * 2; //实现了Interface1的对象实例
        Interface1 i2 = i -> i * 2; //这种是最常见的写法
        Interface1 i3 = (int i) -> i * 2;
        Interface1 i4 = (int i) -> {
            System.out.println("多行的写法");
            return i * 2;
        };
    }
}

5. 函数式接口

1)只需要知道输入输出的类型
2)支持链式操作
2)函数接口默认省略了第一个参数位置为类名,可以加上,如非静态方法引用中使用类名方法引用 和 无参数的构造函数的方法引用


常用的函数式接口

3)Function:定义一个输入是int类型,输出是String的函数接口。使用函数式接口,可以不用定义那么多接口,还可以实现链式编程。示例如下MoneyDemo:

class MyMoney
{
    private final int money;
    public MyMoney(int money)
    {
        this.money = money;
    }
    public void getMoney(Function moneyFormat)
    {
        System.out.println("我的存款是:" + moneyFormat.apply(this.money));
    }
}

public class MoneyDemo
{
    public static void main(String[] args)
    {
        /************ 函数式编程接口的使用 ************/
        //1. 函数接口Function
        MyMoney me = new MyMoney(99999999);
        me.getMoney(i -> new DecimalFormat("#,###").format(i));
        Function moneyFormat = i -> new DecimalFormat("#,###").format(i); //实现了输入是Integer,输出是String的接口对象实例
        me.getMoney(moneyFormat.andThen(s -> "人民币" + s));

        //2. 断言接口Predicate 建议使用带类型的接口,这样就不用写泛型了,如IntConsumer
        IntPredicate predicate = i -> i > 0;
        System.out.println(predicate.test(-1));

        //3. 消费者接口Consumer
        Consumer consumer = s -> System.out.println(s);
        consumer.accept("输入的数据");
    }
}

运行效果

我的存款是:99,999,999
我的存款是:人民币99,999,999
false
输入的数据

6. Lambda方法引用

1)静态方法引用
  使用类名引用方法:类名::方法名
2)非静态方法引用
  a)非构造函数方法引用
    i) 使用对象实例引用方法:实例::方法名
    ii)使用类名引用方法:类名::方法名
  b)构造函数方法引用
    i) 使用类名引用方法:类名::方法名

class Dog {
    private String name = "哮天犬";
    private int food = 50;

    public Dog() {}

    public Dog(String name) {
        this.name = name;
    }

    public static void bak(Dog dog) {
        System.out.println(dog + "叫了");
    }

    public int eat(int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

    @Override
    public String toString() {
        return this.name;
    }
}
 
public class MethodRefrenceDemo {
    public static void main(String[] args) {
        //1.静态方法引用
        Consumer consumer = Dog::bak;
        Dog dog = new Dog();
        consumer.accept(dog);

        //2.使用对象实例的方法引用
        //2.1 非静态,使用对象实例的方法引用
        /*Function function = dog::eat;
        System.out.println("还剩下:" + function.apply(2) + "斤");*/
        
        //2.2 非静态,方法的输入和输出类型一致时,可改成一元函数接口
        /*UnaryOperator function = dog::eat;
        System.out.println("还剩下:" + function.apply(2) + "斤");*/
        
        //2.3 非静态,方法的输入和输出类型一致时,可改成无参函数接口
        IntUnaryOperator function = dog::eat;
        System.out.println("还剩下:" + function.applyAsInt(2) + "斤");

        //dog置空,不影响下面的函数执行,因为java参数是传值(在内部类里引用外部类的变量,外部类的变量不能被修改)
        dog = null;
        System.out.println("还剩下" + function.applyAsInt(2) + "斤");

        //3. 非静态,使用类名来方法引用
        Dog dog2 = new Dog();
        BiFunction biFunction = Dog::eat;
        System.out.println("还剩下:" + biFunction.apply(dog2, 6) + "斤");

        //4. 非静态,无参构造函数的方法引用
        Supplier supplier = Dog::new;
        System.out.println("创建了新对象:" + supplier.get());

        //5. 非静态,有参构造函数的方法引用
        Function stringDogFunction = Dog::new;
        System.out.println("创造了一个新的对象:" + stringDogFunction.apply("旺旺"));
    }

运行效果

哮天犬叫了
吃了2斤狗粮
还剩下:48斤
吃了2斤狗粮
还剩下46斤
吃了6斤狗粮
还剩下:44斤
创建了新对象:哮天犬
创造了一个新的对象:旺旺

3)扩展:

  • jdk默认把当前实例对象作为参数,放到实例方法(非静态方法)的第一个参数,参数名为this。可通过两种方法证明:
    a)修改非静态方法eat的参数,增加Dog this参数,并调用(只传入第二个参数):
    public int eat(Dog this, int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }
    //调用时只传入一个参数
    dog.eat(3)

代码执行不报错:当我们对非静态方法做方法引用的时候,实际上第一个位置是有参数的,是当前实例this。
b)查看反编译里面方法的本地变量表


非静态方法参数

静态方法参数
  • java里变量都是传值,而不是传引用
    在MethodRefrenceDemo.java里虽然48行dog = null;将实例置空了,但是44行创建lambda表达式的时候传的是值,对象实例还在,所以49行不会报错。即:使用lambda表达式,用实例对方法做引用的时候,如果后面将实例置空,前面引用里面的对象不受影响。

7. Lambda类型推断

作用:因为lambda表达式最终会返回一个实现了指定接口的对象实例(匿名类),所以需要有地方告知lambda表达式实现的哪个接口,类型推断就是为了告诉我们lambda表达式实现的是哪个接口。一般通过接口来定义,需要注意的是:当有方法重载,类型冲突时,通过强转解决。

@FunctionalInterface
interface IMath {
    int add(int x, int y);
}

@FunctionalInterface
interface IMath2 {
    int sub(int x, int y);
}

public class TypeDemo {
    public static void main(String[] args) {
        // 变量类型定义
        IMath lambda = (x, y) -> x + y;

        // 数组里
        IMath[] lambdas = {(x, y) -> x + y};

        // 强转
        Object lambda2 = (IMath) (x, y) -> x + y;

        // 通过返回类型
        IMath createLambda = createLambda();

        TypeDemo demo = new TypeDemo();
        // 只有一个test接口时,不需要强转
        // demo.test((x, y) -> x + y);
        // 当有二义性的时候(函数重载),使用强转对应的接口解决
        demo.test((IMath2) (x, y) -> x + y);
    }

    public void test(IMath math) {}

    public void test(IMath2 math) {}

    public static IMath createLambda() {
        return (x, y) -> x + y;
    }
}

8. lambda表达式的变量引用

因为lambda表达式最终会返回一个实现了指定接口的对象实例(匿名类)。所以lambda表达式引用变量跟匿名类引用变量的规则一样。Jdk1.8之前,匿名类要引用外部的变量,外部的变量需要声明为final,是一个常量,实例化之后不会再改了。jdk1.8以后实际也是需要的,默认可以不写,不加final关键字。

public class VarDemo {
    public static void main(String[] args) {
        String string = "我们的时间"; //该语句实际上是:final String string = "我们的时间";
        //string = ""; //会报错:Variable used in lambda expression should be final or effectively final
        Consumer consumer = s -> System.out.println(string + s);
        consumer.accept("12:00");
    }
}

为什么匿名类引用外部变量必须是final?
答案:在java里没有引用的概念,java中只要定义变量就会开辟一个存储单元。因此,对java语言来说只有值传递,没有引用传递。如果外部变量可以改变,改变了外部变量的值,则与传入匿名类里的变量不一样了,会产生二义性。所以在内部类里引用外部类的变量时,外面的变量不能修改,保证里外变量指向同一个对象。

传值图解:


传值图解

说明:2是从1复制而来的,是两个变量,指向的都是new ArrayList<>(),如果把1给了另一个list,1和2就指向了不同的对象了

传引用图解:
说明


传引用图解

9. 级联表达式和柯里化

1)级联表达式:有多个箭头的lambda表达式
2)柯里化:把多个参数的函数转换为只有一个参数的函数
柯里化的目的:函数标准化(柯里化后,所有函数都只有一个参数,调用灵活,如把函数组合起来。)
3)高阶函数:就是返回函数的函数

public class CurryDemo {
    public static void main(String[] args) {
        // 实现了x+y的级联表达式
        Function> fun = x -> y -> x + y;
        System.out.println(fun.apply(2).apply(3));

        Function>> fun2 = x -> y -> z -> x + y + z;
        System.out.println(fun2.apply(2).apply(3).apply(4));

        int[] nums = {2, 3, 4};
        Function f = fun2;

        for (int i = 0; i < nums.length; i++) {
            if (f instanceof Function) {
                Object obj = f.apply(nums[i]);
                if (obj instanceof Function) {
                    f = (Function) obj;
                } else {
                    System.out.println("调用结束:结果为" + obj);
                }
            }
        }
    }
}

执行结果

5
9
调用结束:结果为9

10. lambda表达式中的this

lambda表达式最终会返回一个实现了指定接口的对象实例(匿名类),看上去和内部匿名类很像,但有一个最大的区别就是代码里面的this,内部匿名类this指向的就是匿名类,而lambda表达式里面的this指向的当前类

public class ThisDemo {
    private String name = "ThisDemo";

    public void test() {
        //匿名类实现
        new Thread(new Runnable() {
            private String name = "Runnable";

            @Override
            public void run() {
                System.out.println("这里的this指向匿名类:" + this.name);
            }
        }).start();

        //lambda实现
        // 下面会自动生成lambda$0方法,由于使用了this,所以是非static方法
        new Thread(() -> System.out.println("这里的this指向当前的ThisDemo类:" + this.name)).start();

        // 下面会自动生成lambda$1方法,由于未使用this,所以是static方法
        new Thread(() -> System.out.println("这里没有引用this,生成的lambda1方法是static的")).start();
    }

    public static void main(String[] args) {
        ThisDemo demo = new ThisDemo();
        demo.test();
    }
}

执行结果

这里没有引用this,生成的lambda1方法是static的
这里的this指向当前的ThisDemo类:ThisDemo
这里的this指向匿名类:Runnable

实现原理
1)lambda表达式里面,会把lambda表达式在本类中生成一个以lambda$lambda表达式所在函数名$数字为名的方法
2)该方法是static还是非static,取决于lambda表达式里面是否引用了this。使用了this是非static方法;未使用this是static方法。这就是为什么lambda表达式里面的this指向的是本类,因为他在本类里面创建了一个方法,然后把lambda表达式里面的代码放进去,代码就是在本类的一个方法里面执行的。
3)自动生成的方法是否带参数取决于lambda是否有参数,例子中表达式没有参数(箭头左边是空的),所以自动生成的也没有。

使用javap -s -p 类名查看反编译内容:

反编译内容

未完待续......

下一篇:https://www.jianshu.com/p/cae6bb6c2735

你可能感兴趣的:(SpringBoot2.0不容错过的新特性 WebFlux响应式编程-学习笔记(一))