最近想系统性的学习一下Spring Boot和Spring Cloud,然而在学习SpringBoot的过程中遇到了WebFlux这座大山翻越不过去了,为了能够对Web Flux有个入门级别的了解,学习了晓风轻老师的相关内容,把这段时间对Web Flux的学习历程记录一下:
- 函数式编程和Lambda表达式
- Stream流编程
- reactive响应式流
- webflux开发
本篇文章主要是对函数式编程和lambda表达式的学习做一个简单的笔记,欢迎各位指正。
函数式编程
为什么要用函数式编程?
作为一个Java开发人员,在lambda出现之前,一直使用的是指令式编程,指令式编程关注的点是怎么做,而函数式编程关注的是做什么,无需关注细节。这里面最关键的点就是一个是关注怎么做,而一个关注的是做什么。可能这么说有点抽象,我举个例子说明下,假设我们要从一堆数字里面找出最小的数字,如果用指令式变成怎么做?
public class MinDemo {
public static void main(String[] args) {
int[] nums = {1,2,3,-1,-2,-3};
int min = Integer.MAX_VALUE;
for (int i : nums) {
if(i < min) {
min = i;
}
}
}
}
这段代码对Java程序员来说不算什么,不过我们可以看到这里面写了for循环,for循环里面又定义了一些临时变量,这些就是指令式编程的特点,你想获得一个最小的数,就要关注怎么去做,自己去思考步骤。
回到函数式编程上面来。如果一个不懂code的业务人员让他从一堆数字中找小数字怎么做?没错,财务人员会打开Excecl,然后使用Excel中的min函数,选中相关的单元格就完事儿了。不需要定义一堆变量,不需要再写for循环了,关注的就是要获得最小的数字。那么在Java中怎么用函数式编程呢,还是根据上面的例子改写一下:
public class MinDemo {
public static void main(String[] args) {
int[] nums = {1,2,3,-1,-2,-3};
// IntStream是一个数字流
int min = IntStream.of(nums).min().getAsInt();
System.out.println(min);
}
}
这里面我们在也看不到任何的for循环了,里面最关键的min函数,帮我们搞定了一切,也就是我前面所说的关注做什么,min就是目前要关注的。
这个例子很简单,但是我们可以发散一下,如果说我想从超级多的一堆数字中找到最小的数字,这个时候我们该怎么做呢?在以前的Java中,我们可能要使用多线程来拆分数组,然后分开计算最小值,那么有很多很多的细节需要考虑,但是如果使用函数式编程怎么做,例子如下:
public class MinDemo {
public static void main(String[] args) {
int[] nums = {1,2,3,-1,-2,-3};
// IntStream是一个数字流
int min = IntStream.of(nums).min().parallel().getAsInt();
System.out.println(min);
}
}
什么new Thread都看不见了,一个parellel搞定了一切,这就是函数式编程的强大所在!
lambda表达式
lambda表达式,用晓轻风老师最经典的一句话来说就是,它返回的是一个实现了指定接口的对象实例。什么意思呢,还是例子来说明:
public static void main(String[] args) {
Object target = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
new Thread((Runnable) target).start();
}
想启动一个线程,jdk8之前,需要实现一个Runnable接口,然后new Thread来start,麻烦不?麻烦。那么jdk8之后,我们可以使用lambda表达式来解决这个问题:
public static void main(String[] args) {
// jdk8 lambda
Runnable target = () -> System.out.println("ok");
new Thread(target).start();
}
没错()-> System.out.println("ok")替代了那一大堆什么恶心的override,public void run之类的东西。
->的左边表示输入参数,右边是表达式。我们这里只有一个(),表示输入参数为空,确实,public void run方法本来就没有输入参数。右边的东西就是run方法中的逻辑。
有的同学可能要问,凭什么你这个就可以覆盖run方法,不指定的话,JVM怎么知道你这个覆盖的是其他的什么方法?OK,别着急我们先一个一个的说。在本节的开头,我说过,lambda表达式返回的是实现了一个接口的对象实例。OK,用一个例子来说一些lambda的常见写法,然后再来解释这个问题:
我们先简单说一下,lambda表达式的几种写法:
interface Interface1 {
int doubleNum(int i);
}
public class LambdaDemo1 {
public static void main(String[] args) {
Interface1 i1 = (i) -> i * 2;
// 最常用写法
Interface1 i2 = i -> i * 2;
Interface1 i3 = (int i) -> i * 2;
Interface1 i4 = (int i) -> {
System.out.println("-----");
return i * 2;
};
}
}
定义了一个Interface1接口,要对这个接口实现
main函数里面用4种lambda表达式对其进行了实现,其中最常用的是第二种,可以看到,和之前实现Runnable类似,左边是输入参数,右边是执行体,返回的是实现接口的实例。这里的Interface1有一个doubleNum方法,这个方法有一个参数,所以我们看到箭头的左边不再是一个括号,而有一个参数,参数的名字可以随便命名,右边就是具体实现。奇怪为什么,我们没有指定方法名,就知道要实现的是doubleNum方法呢,可能聪明的你已经知道了,那就是接口里面只能有一个待实现的方法,记住这一点非常关键。而且注意措辞,是待实现的方法,而且只有一个。所以这也是jdk8引入的一个新的概念,叫做函数接口。
@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;
}
}
public class LambdaDemo1 {
public static void main(String[] args) {
Interface1 i1 = (i) -> i * 2;
// 最常用写法
Interface1 i2 = i -> i * 2;
Interface1 i3 = (int i) -> i * 2;
Interface1 i4 = (int i) -> {
System.out.println("-----");
return i * 2;
};
}
}
比如这样写也是对的,接口里面有3个方法,照样可以,但是只有一个待实现的方法,不过有兴趣的同学再加一个待实现的方法试试,报错妥妥的。
函数接口
OK,接下来再看一个使用格式化的方式展示用户存款的例子:
interface IMoneyFormat{
String format(int money);
}
class MyMoney {
private final int money;
public MyMoney(int money) {
this.money = money;
}
public void printMoney(IMoneyFormat moneyFormat) {
System.out.println("我的存款:" + moneyFormat.format(this.money));
}
}
public class MoneyDemo {
public static void main(String[] args) {
MyMoney me = new MyMoney(99999999);
me.printMoney(i -> new DecimalFormat("#,###").format(i));
}
}
从这里我们可以看出,为了实现这个接口,lambda表达式的输入是int类型的money,其执行体就是new DecimalFormat("#,###").format(i)。其实我们看到lambda表达式不需要关注接口是什么,它其实关注的是输入是什么,输出是什么!所以我们使用一个jdk8提供的函数接口来替代一下IMoneyFormat,否则会定义很多接口,这么说很抽象,老规矩,上例子:
class MyMoney {
private final int money;
public MyMoney(int money) {
this.money = money;
}
public void printMoney(Function<Integer, String> moneyFormat) {
System.out.println("我的存款:" + moneyFormat.apply(this.money));
}
}
public class MoneyDemo {
public static void main(String[] args) {
MyMoney me = new MyMoney(99999999);
Function<Integer, String> moneyFormat = i -> new DecimalFormat("#,###")
.format(i);
// 函数接口链式操作
me.printMoney(moneyFormat.andThen(s -> "人民币 " + s));
}
}
在这里我们已经看不到接口了,而是用了一个Function来替代,这个Function就是jdk8提供的函数接口,Function<Integer,String>告诉了关注点----输入是整数,输出是字符串,正巧就是我们当时IMoneyFormat接口里面的方法要表达的意思。所以这里多次强调,关注的是输入和输出,什么函数名称之类的都不重要了,实现细节不重要了,关注what you want!同时这里我们还用了一个链式调用,也就是andThen,这也是函数接口的一个好处,上一个函数的输出,是下一个函数的输入,s->“人民币”+s,其中的s就是moneyFormat format后的结果。这里简单看一下andThen的源码:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
我们可以看到,新返回的lambda表达式的输入仍然是第一个lambda表达式的输入,这里也就是moneyFormat的输入,执行体里面我们可以看到有两层,after.apply(apply(t)),很明显首先执行最里面的apply(t),也就是moneyFormat的apply,执行完了后,再执行after的apply。
常用函数接口
接口 | 输入参数 | 返回类型 | 说明 |
---|---|---|---|
Predicate<T> | T | boolean | 断言 |
Consumer<T> | T | / | 消费一个数据 |
Function<T,R> | T | R | 输入T输出R的函数 |
Supplier<T> | / | T | 提供一个数据 |
UnaryOperator<T> | T | T | 一元函数(输出输入类型相同) |
BiFunction<T,U,R> | (T,U) | R | 2个输入的函数 |
BinaryOperator<T> | (T,T) | T | 二元函数(输出输入类型相同) |
先看一看Predicate和Consumer的用法:
public class FunctionDemo {
public static void main(String[] args) {
// Predicate<Integer> predicate = i->i>0;
IntPredicate predicate = i -> i > 0;//用这种方式可以不用写反泛型了
System.out.println(predicate.test(-9));
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("输入的数据");
}
}
Predicate<Integer>和IntPredicate都是断言函数,只不过IntPredicate相比于Predicate来说,指定了输入必须是整形。它主要用来做断言逻辑的,所以它的返回值是Boolean,其实用Funtion也可以实现这个功能,那就是Function<Integer,Boolean>也可以实现类似IntPredicate的功能。在我后面讲Stream的时候,filter过滤器里面用的就是这个Predicate。
接下来是Consumer,Consumer是一个消费者,消费者就是只有输入,没有返回。这个就相当于一个爱老婆的程序员,他的老婆就是个消费者,输入就是程序员的工资卡,没有返回,老婆最多给你println一个桃心儿就可以了,但是println是没有返回值的啊!!!所以我们可以看到consumer的accpet了数据后,直接println就完了。后面的Stream流中的peek用于debug打印一些数据元素的时候,就会用到这个Consumer。
方法引用
在前面讲的lambda表达式中,我们经常看到s -> System.out.println(s),其实有一种简化版的写法,System.out::println,相信很多小伙伴都看到过这种写法,这就要求你的lambda表达式的左边是一个输入参数,右边的执行体用到的参数和左边的输入参数完全一样的时候,就可以简写成这样。例子如下:
public class MethodReferenceDemo {
public static void main(String[] args) {
// 1.当执行体只有一个函数调用
// 2.函数参数跟箭头左边一样,可以缩写
// Consumer<String> consumer = s->System.out.println(s);
Consumer<String> consumer = System.out::println;
consumer.accept("接受的数据");
}
}
其实这里我不是要说明如何去简写这些执行体,而是为了让大家知道lambda表达式中的方法引用。一共有如下几种方法引用:
- 静态方法引用
class Test { private String name = "静态方法"; public static void method(Test test) { System.out.println(test + "被调用了"); } @Override public String toString() { return this.name; } } public class MethodReferenceDemo { public static void main(String[] args) { Test test = new Test(); // 静态方法的方法引用 Consumer<Test> consumer = Test::method; consumer.accept(test); } }
这个例子中,我们使用lambda表达式对Test的静态方法进行了引用,跟System.out.println的很像,也就是类名::静态方法名的形式。我们这里的Consumer消费是一个Test对象,为什么呢,因为我们的consumer引用的是Test的method方法,这个方法的输入是什么?是Test对象,所以这里再次强调,函数式编程关心的是输入和输出,关心的是你要的是什么。不要再像指令编程一样,纠结于怎么去实现~~
- 非静态方法引用
class Test {
private int salary = 10;
public int useMoney(int num) {
System.out.println("用了" + num + "W");
this.salary-= num;
return this.salary;
}
@Override
public String toString() {
return this.name;
}
}
public class MethodReferenceDemo {
public static void main(String[] args) {
Test test = new Test();
// 非静态方法,使用对象实例引用
IntUnaryOperator function = test::useMoney;
test = null;
System.out.println("还剩下" + function.applyAsInt(2) + "W");
}
}
在这里我们为了在lambda表达式中使用了test对象的useMoney方法,所用的语法是对象名::方法名,这里要注意的,我在程序中故意使用了test=null,是想告知大家,我们只是让test这个引用指向了一个空对象,但是function已经指向了对象的 useMoney方法,和test这个引用已经没有了任何关系,所以不会报空指针异常,这一点请注意一下。不过问题不大,实在无法理解的同学,可以不纠结这个点,不影响使用。
使用类名的方法来引用:
class Test {
private int salary = 10;
public int useMoney(int num) {
System.out.println("用了" + num + "W");
this.salary-= num;
return this.salary;
}
@Override
public String toString() {
return this.name;
}
}
public class MethodReferenceDemo {
public static void main(String[] args) {
Test test = new Test();
BiFunction<Test, Integer, Integer> testFunction = Test::useMoney;
System.out.println("还剩下" + testFunction.apply(test, 2) + "W");
}
}
在这里我们使用了一个类名来调用非静态的方法,可能初一看不可思议,只用类名怎么可能调用得到非静态方法。所以这里还是使用了一个test对象,在调用useMoney时,我们传入了两个参数,一个test对象还有一个是我们真正关心的参数。有的同学可能要问了,useMoney不是只有一个参数吗,而且前面讲了函数式编程关心的是输入和输出,这里输入只有一个,却传递了两个,这不是矛盾吗?不矛盾,大家仔细回想一下,在java的非静态方法中,其实默认第一个参数是该对象,也就是this,useMoney(int num)其实真实面目是useMoney(Test this,int num),这也是为什么我们要传递两个参数的原因!
类型推断
@FunctionalInterface
interface IMath {
int add(int x, int y);
}
@FunctionalInterface
interface IMath2 {
int add2(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();
// 当有二义性的时候,使用强转对应的接口解决
demo.test((IMath) (x, y) -> x + y);
}
public void test(IMath math) {
}
public void test(IMath2 math) {
}
public static IMath createLambda() {
return (x, y) -> x + y;
}
}
代码中的注释已经比较详细了,我们可以看到当我们用某种接口类型声明一个变量时,它的右边都是使用了lambda表达式来实现这个接口。包括代码的最后我们还用了一个createLambda方法来返回一个实现了IMath接口的实例,IMath中只有一个待实现的方法,这个方法有两个参数,所有箭头的左边是(x,y)分别表示两个参数,箭头的右边是具体的执行体。唯一需要注意的是代码中的两个test方法,如果说你有两个接口IMath和IMath2,这两个接口的待实现方法的输入和输出一样时,在调用test方法时,必须要注明接口类型,否则会出错,有兴趣的同学可以把 demo.test((IMath) (x, y) -> x + y);中的(IMath)去掉,会发现有二义性错误。这就是lambda的类型推断
变量引用
public class VarDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();//第一个list
//list = new ArrayList<String>();
Consumer<String> consumer = s -> System.out.println(s + list);//第二个list
consumer.accept("1211");
}
}
其实在lambda表达式中,我们要引用的变量类型自动已经变成了final类型,虽然我这里的list变量没有用final修饰,但是当使用在lambda表达式中后,这个list就已经是final类型,list不能再指向别的元素,但是可以往里面添加元素和删除元素。这里我想多讲几句,尤其是对匿名实现类为什么要引用final类型的变量不清楚的同学可以看下去,如果已经清楚的可以不看了。我们知道java的传值传的是值而不是引用,什么意思呢,看代码中我的注解。第一个list指向了一个ArrayList对象,当传递给lambda表达式中的变量时,这个list是一个新声明的变量副本,也就是说现在有两个list变量,但是指向的是同一个对象。如果说,第一个list不是final类型的,传递给了里面的list,这个时候两个list是两个变量,但是指向的是同一个对象,然后第一个list不小心被指向了另外一个对象,那么就会导致第二个list操作的对象和第一个list对象无任何关系了,会出现数据错误,这就是java为什么要求在被匿名实现类引用的外部变量必须用final的原因!
级联表达式和柯里化
什么是柯里化:把多个参数的函数转换为只有一个参数的函数
为什么要柯里化:函数标准化
估计又说抽象了,还是上个例子结束对lambda表达式的学习之旅
/**
* 柯里化:把多个参数的函数转换为只有一个参数的函数 目的:函数标准化
* 高阶函数:函数的函数
*/
public class CurryDemo {
public static void main(String[] args) {
// 实现了x+y的级联表达式
Function<Integer, Function<Integer, Integer>> fun = x -> y -> {
return x + y;
};
System.out.println(fun.apply(2).apply(3));
//返回函数的函数
Function<Integer, Function<Integer, Function<Integer, Integer>>> fun2 = x -> y -> z -> x + y + z;
//每个函数只有一个输入参数
System.out.println(fun2.apply(2).apply(3).apply(4));
//可以循环调用fun2
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);
}
}
}
}
}
柯里化平时工作中用的不多,但是听老师说,编写框架的大神会经常用到,所以这里就不说太多了,能力有限,把自己理解到的东西分享给大家,就写到这么多啦~