[TOC]
最近太忙了,发文频率有点不稳定,理解万岁。前面和大家说了要更 WebFlux,学习 WebFlux 之前,我们先来学习一些前置知识。
Rome was not built in a day。
WebFlux 也不是一帮人拍脑门突然发明的,它是一个漫长的过程,WebFlux 本身在逐步完善,各种配套工具/理论也在逐步发展。
因此当松哥想写 WebFlux 的时候,发现没法直接从 WebFlux 本身开始写起,对于很多没有接触过函数式编程的人来说,上来就整 WebFlux 还是有一些挑战的,想来想去,我觉得还是先来和大家捋一捋 JDK8 中的一些旧玩意。
虽然 JDK8 发布距今已经七八年了,但是相信还是有相当多小伙伴用着 JDK8,写着 JDK6 的代码。所以我们有必要回顾一下 JDK8,也算是我们学习 WebFlux 的一些前置知识。
好啦,开整吧。
1.Lambda 表达式的四种写法
JDK8 中引入了 Lambda,这个大家都知道,虽然现在 JDK 都出到 16 了,但是老实说,项目中的 Lambda 表达式似乎还是很少有人用。有的团队技术风格激进,可能会见到很多 Lambda,但是大部分技术团队还是比较保守的。今天为了学习 WebFlux,我们还是先来回顾一下 Lambda 表达式的几种写法。
先来说说,如果要用 Lambda,必须是只有一个需要强制实现方法的接口,我们可以使用 @FunctionalInterface
注解去标记该接口:
1 2 3 4
| @FunctionalInterface interface ICalculator{ int square(int i); }
|
此时如果该接口中有多个空方法,编译期间就会报错。
现在我们建议尽量将一个接口设计的小一些,这样也满足单一职责原则。
不过 JDK8 中引入了 default 方法,就是自带默认实现的那种,自带默认实现的方法可以有多个,这个并不影响 Lambda,并且 @FunctionalInterface
注解也不会去检查默认方法的数量。
1.1 单个参数的
如果只是一个参数,那么直接写参数即可,例如如下代码:
1 2 3 4 5 6 7 8 9 10
| interface ICalculator{ int square(int i); } public class LambdaDemo01 { public static void main(String[] args) { ICalculator ic = i -> i * i; int square = ic.square(5); System.out.println("square = " + square); } }
|
当函数只有一个参数的时候,直接写即可,不需要添加 ()
。
1.2 多个参数
多个参数的话,就需要写上 ()
了,以 Spring Security 中登录成功的回调为例(不了解 Spring Security 的小伙伴可在公号后台回复 ss):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| .defaultLogoutSuccessHandlerFor((req,resp,auth)->{ resp.setContentType("application/json;charset=utf-8"); Map<String, Object> result = new HashMap<>(); result.put("status", 200); result.put("msg", "使用 logout1 注销成功!"); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(result); resp.getWriter().write(s); },new AntPathRequestMatcher("/logout1","GET")) .defaultLogoutSuccessHandlerFor((req,resp,auth)->{ resp.setContentType("application/json;charset=utf-8"); Map<String, Object> result = new HashMap<>(); result.put("status", 200); result.put("msg", "使用 logout2 注销成功!"); ObjectMapper om = new ObjectMapper(); String s = om.writeValueAsString(result); resp.getWriter().write(s); },new AntPathRequestMatcher("/logout2","POST")) .and() .csrf().disable();
|
这种情况,方法有多个参数,此时使用 Lambda 表达式就需要加上 ()
。
1.3 要写参数类型的
正常来说用 Lambda 时候不需要写上参数类型,但是如果你需要写,就要加上 ()
,还是上面那个例子,如下:
1 2 3 4 5 6 7 8 9 10
| interface ICalculator{ int square(int i); } public class LambdaDemo01 { public static void main(String[] args) { ICalculator ic = (int i) -> i * i; int square = ic.square(5); System.out.println("square = " + square); } }
|
1.4 方法体不止一行的
如果方法体不止一行,需要用上 {}
,如果方法体只有一行,则不需要 {}
,参考上面 2、3。
2.函数接口
JDK8 中自带了函数式接口,使用起来也非常方便。
2.1基本应用
我们先来看一个简单的例子。
假设我有一个打招呼的接口 SayHello,SayHello 接口中只有一个 sayHello 方法,然后在 User 类中调用该接口对应的方法,最终用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @FunctionalInterface interface SayHello { String sayHello(String name); }
class User { private String username;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String say(SayHello sayHello) { return sayHello.sayHello(this.username); } }
public class LambdaDemo02 { public static void main(String[] args) { User user = new User(); user.setUsername("javaboy"); String say = user.say((username) -> "hello " + username); System.out.println("say = " + say); } }
|
分析 main 方法中的调用过程之后,我们发现,在调用时最核心的是如下一行代码:
1
| (username) -> "hello " + username
|
在这段代码中,我们只关心方法的输入和输出,其他的都不是我所考虑的,为了一个简单的输入输出,我还要额外定义一个接口,这显然不太划算。
JDK8 中提供了函数接口,可以帮助我们简化上面的接口定义。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class User2 { private String username;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String say(Function<String,String> sayHello) { return sayHello.apply(this.username); } } public class LambdaDemo03 { public static void main(String[] args) { User2 user2 = new User2(); user2.setUsername("javaboy"); String say = user2.say((username) -> "hello " + username); System.out.println("say = " + say); } }
|
可以用 Function<String,String>
代替我们前面的接口定义,这里有两个泛型,第一个泛型表示接口输入的参数类型,第二个泛型表示接口输出的参数类型,而且大家注意,我们最终 main 方法中的调用方式是不变的。有了 Function 函数之后,以后我们就不需要定义一些简单的接口了。
而且 Function 函数还支持链式操作,如下:
1 2 3 4 5 6 7 8 9
| public class LambdaDemo03 { public static void main(String[] args) { User2 user2 = new User2(); user2.setUsername("javaboy"); Function<String, String> func = (username) -> "hello " + username; String say = user2.say(func.andThen(s -> "你好 " + s)); System.out.println("say = " + say); } }
|
2.2 其他函数接口
接口 |
输入参数 |
返回类型 |
说明 |
UnaryOperator |
T |
T |
一元函数,输入输出类型相同 |
Predicate |
T |
boolean |
断言 |
Consumer |
T |
/ |
消费一个数据,只有输入没有输出 |
Function<T,R> |
T |
R |
输入 T 返回 R,有输入也有输出 |
Supplier |
/ |
T |
提供一个数据,没有输入只有输出 |
BiFunction<T,U,R> |
(T,U) |
R |
两个输入参数 |
BiPredicate<L, R> |
(L,R) |
boolean |
两个输入参数 |
BiConsumer<T, U> |
(T,U) |
void |
两个输入参数 |
BinaryOperator |
(T,T) |
T |
二元函数,输入输出类型相同 |
接下来我们来看看这些函数接口。
2.2.1 UnaryOperator
当输入输出类型相同时,可以使用 UnaryOperator 函数接口,例如我们上面的代码,修改之后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class User2 { private String username;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String say(UnaryOperator<String> sayHello) { return sayHello.apply(this.username); } } public class LambdaDemo03 { public static void main(String[] args) { User2 user2 = new User2(); user2.setUsername("javaboy"); UnaryOperator<String> func = (username) -> "helloo " + username; String say = user2.say(func); System.out.println("say = " + say); } }
|
2.2.2 Predicate
Predicate 输入一个 T 类型的参数,输出一个 boolean 类型的值。
举一个简单的例子,例如如下代码,我们定义一个 List 集合中存放着用户姓名,现在要过滤出所有姓张的用户,代码如下:
1 2 3 4 5 6 7 8 9
| public class LambdaDemo04 { public static void main(String[] args) { List<String> names = Arrays.asList("张三", "里斯", "张五"); List<String> list = names.stream().filter(s -> s.startsWith("张")).collect(Collectors.toList()); for (String s : list) { System.out.println("s = " + s); } } }
|
filter 中传入的就是一个 Predicate 函数接口,这个接口接收 String 类型的数据,返回一个 boolean。
注意
一些常用类型的函数接口,JDK 中直接提供了相关的类供我们使用,例如 Predicate<Integer>
可以用 IntPredicate
代替;Consumer<Integer>
可以用 IntConsumer
代替。
2.2.3 Consumer
看名字就知道,这个是消费数据,只有输入没有输出。
例如集合的遍历就可以使用 Consumer 函数接口。
1 2 3 4 5 6
| public class LambdaDemo04 { public static void main(String[] args) { List<String> names = Arrays.asList("张三", "里斯", "张五"); names.stream().forEach(s -> System.out.println(s)); } }
|
2.2.4 Supplier
Supplier 刚好和 Consumer 相反,它只有输出没有输入。有的时候我们的工厂方法没有输入只有输出,这个时候就可以考虑使用 Supplier(如果有输入参数,则可以考虑使用 Function 函数接口)。
1 2 3 4 5 6 7 8 9 10
| Supplier<Connection> supplier = ()->{ Connection con = null; try { con = DriverManager.getConnection("", "", ""); } catch (SQLException e) { e.printStackTrace(); } return con; }; Connection connection = supplier.get();
|
3.小结
其实 WebFlux 前置知识还是蛮多的,今天先聊这些吧,我们后面继续。