本文的思想来自于 此课程教学视频, 是课程学习的笔记整理,希望进行更详细学习的小伙伴请移步原课程学习 ~~
我愿意称函数式编程为 IDEA 的 Ctrl + Enter 大法 ~~~
一、什么是函数式编程 + 为什么要学习函数式编程
函数式编程是一种编程范式,其将关注点从对象转变为函数,并且具有简洁开发快速、接近于自然语言、易于并发编程等优点。
二、 函数式编程基础 —— Lambda 表达式
1. 简单理解
其是一种匿名内部类的优化写法,将匿名内部类(只含有一个方法的匿名内部接口)只留下 函数参数列表 和 方法的实现 。
(参数列表) -> {
// 方法实现
}
其将关注点转移到 参数列表 和 方法实现 , 省略掉无意义的创建代码 ,请看下面的代码示例 :
可以先写出匿名内部类的形式,再转换为 Lambda 表达式 , 通过 Alt + Enter 快捷键,, 此快捷键也可以把我们的 Lambda 表达式化为最简格式
2. 代码示例
public class LambdaTest {
public static void main(String[] args) {
// 可以先写出 匿名内部类的 形式, 再转化为Lambda表达式
foreachArr((value) -> {System.out.println(value)});
}
public static void foreachArr(IntConsumer consumer){
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
for (int i : arr){
consumer.accept(i);
}
}
}
3. 函数式编程如何实现简化代码
请看如下的例子 ——
我们需要输入给定数组中的奇数和偶数,如果没有 Lambda 表达式,我们是不是需要写两个函数,一个判断奇数的方法 , 一个判断偶数的方法 。
但是,有了函数式编程,我们可以只写一个方法,通过在调用方法时为接口的方法传入不同的实现,来实现输出奇数和偶数 。
public class LambdaTest {
public static void main(String[] args) {
// 输出偶数
foreachArr(value -> {
if(value % 2 == 0){
System.out.println(value);
}
});
// 输出奇数
foreachArr(value -> {
if(value % 2 == 1){
System.out.println(value);
}
});
}
public static void foreachArr(IntConsumer consumer){
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
for (int i : arr){
consumer.accept(i);
}
}
}
三、 Stream 流
1. 什么是 Stream 流
Stream 流是函数式编程的一种模式,其用于对集合和数组进行流式操作 。
示例 : 打印所有 年龄小于 18 的作家的集合
/**
* @Author: WanqingLiu
* @Date: 2023/03/04/22:32
*/
public class Test {
public static void main(String[] args) {
List<Author> authors = gerAuthors();
// 打印所有年龄小于 18 的作家的集合
authors.stream() // 把集合转换为 流
.distinct() // 去重操作
.filter(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge() < 18;
}
}) // 过滤操作
.forEach(new Consumer<Author>() {
@Override
public void accept(Author author) {
System.out.println(author.getName());
}
});
}
public static List<Author> gerAuthors(){
Author author1 = new Author(1L, "晚晴1", 21, null);
Author author2 = new Author(2L, "晚晴2", 15, null);
Author author3 = new Author(3L, "晚晴3", 13, null);
Book book1 = new Book(1L, "Java", "技术上", 30);
Book book2 = new Book(2L, "C++", "技术上", 40);
Book book3 = new Book(3L, "Python", "技术上", 50);
// Book book4 = new Book(4L, "JavaScript", "技术上", 60);
ArrayList<Book> books1 = new ArrayList<>();
books1.add(book1);
ArrayList<Book> books2 = new ArrayList<>();
books2.add(book1);
books2.add(book2);
ArrayList<Book> books3 = new ArrayList<>();
books3.add(book1);
books3.add(book2);
books3.add(book3);
author1.setBooks(books1);
author2.setBooks(books2);
author3.setBooks(books3);
ArrayList<Author> authors = new ArrayList<>();
authors.add(author1);
authors.add(author2);
authors.add(author3);
return authors;
}
}
2. 使用
请见本文章 :Stream 流 问题
3. 高级优化
3.1 通过转换为基本数据类型避免自动装箱和拆箱
大量的自动装箱拆箱操作会降低程序执行的效率,使用我们需要使用 Stream 流为我们提供的转换为基本数据类型流的方法转换为基本数据类型流,再对基本数据类型进行操作。
authors.stream()
.map(author -> author.getAge())
.map(age -> age + 10) // 发生拆箱操作
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
// 优化上面的代码
authors.stream()
.mapToInt(value -> value.getAge()) // 直接封装为 int
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);
2. 并行流
要要处理的数据分给多个线程去处理 —— parallel 将串行流转化为并行流(也可以直接通过 parallelStream() 直接得到并行流)
public static void testParallel(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Integer sum = stream.parallel()
.filter(integer -> integer > 3)
.reduce((result, curEle) -> result + curEle)
.get();
}
通过 peek 进行调试 ——
public static void testParallel(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5,6,7,8,9,10);
Integer sum = stream.parallel()
.peek(integer -> System.out.println("线程名称:" + Thread.currentThread().getName() + " 当前线程操作的元素:" + integer)) // 进行调试
.filter(integer -> integer > 3)
.reduce((result, curEle) -> result + curEle)
.get();
System.out.println(sum);
}
四、Optional
1. 概述
以对象判断是否为 null 为例子 , Optional 可以帮助我们进行处理, 写出更优雅的代码避免空指针异常 。
Optional 就好像一个包装类, 可以把我们具体的数据封装到 Optional 对象内部,我们使用 Optional 中封装好的方法操作封装进去的数据 。
2. 使用
2.1 创建 Optional 对象
Author author = new Author();
// 调用 ofNullable 方法,把对象封装为非空对象
Optional<Author> authorOption = Optional.ofNullable(author);
authorOption.ifPresent(author1 -> System.out.println(author1.getName()));
public static void main(String[] args) {
Optional<Author> authorOptional = getAuthorOptional();
authorOptional.ifPresent(author -> System.out.println(author.getName()));
}
public static Optional<Author> getAuthorOptional(){
Author author = new Author(1L, "晚晴1", 21, null);
return Optional.ofNullable(author);
}
将 MyBatis 中的数据返回值设置为 Optional , MyBatis 会自动帮我们封装为 Optional 类型
ofNullable 原理 : 判断为 null 时, 使用 empty() 方法创建对象
2.2 安全消费值
如何对 Optional 对象进行消费操作
authorOption.ifPresent(author1 -> System.out.println(author1.getName()));
2.3 获取 Optional 对象中的值
- get —— 推荐不用, null 值会报异常
- orElseGet
// Optional 中为空时,返回我们设置的默认值
authorOptional.orElseGet(() -> new Author());
- orElseThrow
authorOptional1.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("内部对象为 null"))
2.4 数据过滤
filter
private static void testFilter(){
Optional<Author> authorOptional = getAuthorOptional();
// 过滤出大于 18 的类, 年龄小于 18 的类继续被封装为 null
Optional<Author> authorOptional1 = authorOptional.filter(author -> author.getAge() > 18);
}
2.5 数据判断
isPresent
private static void testIsPresent(){
Optional<Author> authorOptional = getAuthorOptional();
if(authorOptional.isPresent()){
// 进行消费操作
}
};
2.6 数据转换
map
private static void testMap(){
Optional<Author> authorOptional = getAuthorOptional();
authorOptional.map(author -> author.getBooks())
.ifPresent(books -> System.out.println(books));
}
五、函数式接口
1. 什么是函数式接口
可以用于进行函数式编程被lambda表达式化简的接口就是函数式接口,其具有如下特点:
- 被
@FunctionalInterface
注解修饰 —— 没有此注释,但是只含一个抽象方法的接口也是函数式接口,其类似于一个提升作用,提升此接口必须有且只有一个抽象方法。 - 只含有一个 抽象 方法
@FunctionalInterface
public interface InterfaceA {
// 只含有一个抽象方法
void function();
}
2. 常见的函数式接口
我们通过接口的 参数类型 和 返回值 决定接口的作用
2.1 Consumer 消费型接口
需要一个参数,返回值为空,只能消费这个参数
2.2 Functional 计算转换型接口
一个参数,一个返回值,参数和返回值泛型不一样
2.3 Predicate 判断型接口
传入一个参数,返回的类型为 boolean 型
2.4 Supplier 生产型接口
没有参数,只有一个返回值
2.5 其他函数式接口
定位到 function 包下,去找自己需要用的
3. 函数式接口中的常见方法
作用于函数式接口 —— 初学者不建议看,常常用于自定义方法
3.1 and
用于拼接两个判断条件 ,使用场景为自定义方法
示例:
// 输出大小为偶数并且名字长度大于1的作家
public static void printNum(IntPredicate predicate1, IntPredicate predicate2 ){
int[] arr = {1, 2, 3, 4, 5};
for (int i : arr){
// 用 and 操作连接两个 predicate
if(predicate1.and(predicate2).test(i)){
System.out.println(i);
}
}
}
public static void main(String[] args) throws Throwable {
printNum(value -> value % 2 == 0, value -> value > 3);
}
3.2 or
// 输出大小为偶数,或者名字长度大于1的作家
public static void testOr(IntPredicate predicate1, IntPredicate predicate2 ){
int[] arr = {1, 2, 3, 4, 5};
for (int i : arr){
if(predicate1.or(predicate2).test(i)){
System.out.println(i);
}
}
}
3.3 negate
// 打印作家年龄小于等于 17 的
public static void testNegate(){
List<Author> authors = gerAuthors();
authors.stream()
.filter(((Predicate<Author>) author -> author.getAge() > 17).negate()).forEach(author -> System.out.println(author.getName()));
}
六、 方法引用
Java 中的语法糖
, 是对 Lambda 表达式的另一种简化 , 其规则比较繁琐,不需要纠结于记忆 。
可以转换为方法引用格式的 Lambda 具有如下特点:
1. 基本格式
类名或者对象名 :: 方法名
2. 推荐用法 *
编译器的 Alt + Enter 快捷键看看能不能转换为方法引用,能转就转
3. 方法应用的语法
3.1 引用类的静态方法
类名::静态方法名
- 方法体中只有一行代码,并且是调用了某个类的静态方法
- 重写的抽象方法的所有参数按照顺序传入此静态方法
3.2 引用对象的实例方法
对象名::方法名
- 方法体只有一行代码,并且其调用某个对象的成员方法
- 重写的抽象方法的所有参数按照顺序传入此成员方法
3.3 引用类的实例方法
类名::方法名
- 方法体只有一行代码,并且其调用第一个参数的成员方法
- 剩余的所有参数按照顺序传入此成员方法中
3.4 构造器引用
类名 :: new
- 只有一行代码,这行代码调用的是构造方法