函数式编程——lambda表达式、Option,stream流
一、概述
1. 学习目的
-
代码可读性
-
避免过分嵌套
-
看懂别人写的代码
-
大数据量下集合处理效率
-
底层使用多线程处理并线程安全可以保障?
查看代码
/**
* 查询未成年作家评分在70分以上的书籍,由于流的影响所以作家和书籍可能会重复出现,所以要去重
* 我不是很理解这里为什么说会重复出现呢?
*/
@Test
public void test1() {
List<Book> bookList = new ArrayList<>();
List<Author> authorList = new ArrayList<>();
Set<Book> uniqueBookValues = new HashSet<>();
Set<Author> uniqueAuthorValues = new HashSet<>();
for (Author author : authorList) {
// 这里如果重复就不会添加成功
if (uniqueAuthorValues.add(author)) {
if (author.getAge() < 18) {
List<Book> books = author.getBookList();
for (Book book : books) {
if (book.getScore() > 70D) {
// 如果之前有这本书就不会再次添加
if (uniqueBookValues.add(book)) {
bookList.add(book);
}
}
}
}
}
}
System.out.println(bookList);
// authorList.add(new Author());
// 函数式写法
List<Book> collect = authorList.stream()
.distinct()
.filter(author -> author.getAge() < 18)
.map(author -> author.getBookList())
.flatMap(Collection::stream)
.filter(book -> book.getScore() > 70)
.distinct()
.collect(Collectors.toList());
System.out.println(collect);
}
2. 函数式编程思想
-
面向对象思想主要是关注对象能完成什么事情,函数式编程思想就像函数式,主要是针对数据操作;
-
代码简洁容易理解,方便于并发编程,不需要过分关注线程安全问题
二、lambda表达式
1. 概述
-
lambda是JDK8中的一个语法糖,可 以对某些匿名内部类的写法进行优化,让函数式编程只关注数据而不是对象。
-
基本格式:(参数列表)->{代码}
2. 实战:将匿名内部类转换为lambda表达式的写法
①可以用al+enter快捷键在常规写法和lambda表达式写法自己自动切换;
②lambda表达式的写法只关注方法的参数和方法体,其余全部可以省略;
查看代码
package com.tzc;
import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntPredicate;
public class LambdaDemo01 {
public static void main(String[] args) {
//案例1:创建线程
//常规写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程中run()方法被执行了");
}
}).start();
//lambda表达式写法
new Thread(() -> {
System.out.println("新线程中run()方法被执行了");
}).start();
//案例2:可以通过alt+enter在两种写法中自动切换
//常规写法
int num1 = calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
});
System.out.println(num1);
//lambda表达式写法
int num2 = calculateNum((left, right) -> left + right);
System.out.println(num2);
//案例3:可以通过alt+enter在两种写法中自动切换
//常规写法
printNum(new IntPredicate() {
@Override
public boolean test(int value) {
return value % 2 == 0;
}
});
//lambda表达式写法
printNum(value -> value % 2 == 0);
//案例3:可以通过alt+enter在两种写法中自动切换
//常规写法
Integer res1 = typeConvert(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
});
System.out.println(res1);
//lambda表达式写法
Integer res2 = typeConvert(s -> Integer.valueOf(s));
System.out.println(res2);
}
public static int calculateNum(IntBinaryOperator operator){
int a = 10, b = 20;
return operator.applyAsInt(a, b);
}
public static void printNum(IntPredicate predicate) {
int[] arr = {1,2,3,4,5,6,7,8,9,10};
for (int i : arr) {
if (predicate.test(i)) {
System.out.println(i);
}
}
}
public static <R> R typeConvert(Function<String, R> function) {
String str = "12345";
return function.apply(str);
}
}
三、stream流
1. 概述
-
stream使用的是函数式编程模式,可以被用来对集合或数组进行链状流式的操作
-
有别于其他输入输出流,这里是针对集合操作数据的流哦
-
创建流实战:
2. 功能
-
流不存储元素。它只是通过计算操作的流水线从数据结构, 数组或I/O通道等源中传递元素。
-
流本质上是功能性的。对流执行的操作不会修改其源。例如, 对从集合中获取的流进行过滤会产生一个新的不带过滤元素的流, 而不是从源集合中删除元素。
-
Stream是惰性的, 仅在需要时才评估代码。
-
在流的生存期内, 流的元素只能访问一次。像Iterator一样, 必须生成新的流以重新访问源中的相同元素。
3. 常用方法说明
数据准备:
Author
@EqualsAndHashCode //用于去重操作
public class Author implements Comparable<Author>{
private Long id;
private String name;
private String introduction;
private Integer age;
private List<Book> bookList;
/**
* 使用sorted时比较整个元素时,要实现比较接口,并重写方法
*/
@Override
public int compareTo(Author o) {
//这里我们规定根据年龄进行排序
// 这里sorted如果输出的是降序,你就把这俩顺序对换就可以了,不用记忆何为升序何为降序
return o.getAge() - this.getAge();
}
//// 初始化一些数据
public static List<Author> getAuthors() {
Author author1 = new Author(1L, "杨杰炜", "my introduction 1", 18, null);
Author author2 = new Author(2L, "yjw", "my introduction 2", 19, null);
Author author3 = new Author(2L, "yjw", "my introduction 2", 19, null);
Author author4 = new Author(4L, "wdt", "my introduction 4", 29, null);
Author author5 = new Author(5L, "wtf", "my introduction 5", 12, null);
List<Book> books1 = new ArrayList<>();
List<Book> books2 = new ArrayList<>();
List<Book> books3 = new ArrayList<>();
// 上面是作者和书
books1.add(new Book(1L, "哲学、爱情", "书名1", 45D, "这是简介哦"));
books1.add(new Book(2L, "历史", "书名2", 84D, "这是简介哦"));
books1.add(new Book(3L, "喜剧", "书名3", 83D, "这是简介哦"));
books2.add(new Book(5L, "战争", "书名4", 65D, "这是简介哦"));
books2.add(new Book(6L, "爱情、动作", "书名5", 89D, "这是简介哦"));
books3.add(new Book(7L, "诗集", "书名6", 45D, "这是简介哦"));
books3.add(new Book(8L, "剧情、历史", "书名7", 44D, "这是简介哦"));
books3.add(new Book(9L, "喜剧", "书名8", 81D, "这是简介哦"));
author1.setBookList(books1);
author2.setBookList(books2);
author3.setBookList(books2);
author4.setBookList(books3);
author5.setBookList(books2);
return new ArrayList<>(Arrays.asList(author1, author2, author3, author4, author5));
}
}
Book
/*
* @author yjiewei
* @date 2022/2/13 15:32
*/
package com.tzc.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Book {
private Long id;
private String category;
private String name;
private Double score;
private String introduction;
}
①流的创建
StreamDemo01
package com.tzc;
import com.tzc.entity.Author;
import com.tzc.entity.Book;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamDemo01 {
public static void main(String[] args) {
List<Author> authors = Author.getAuthors();
//快速入门案例1:如何将集合转换为流
// 找出集合中年龄大于18岁的作家
authors.stream()
//distinct()去重操作依赖于equals()方法,可以重写此方法来自定义去重的标准,
//@EqualsAndHashCode注解生成的equals()方法是所有属性相同就算相同,没有equals()方法则地址相同才算相同
.distinct()
.filter(author -> author.getAge() > 10)
.forEach(author -> System.out.println(author));
//案例2:如何将数组转换为stream流
test01();
}
public static void test01(){
Integer[] nums = {1, 2, 2, 3, 4, 5};
//方式1:Arrays.stream()
// Stream<Integer> stream = Arrays.stream(nums);
//方式2:Stream.of()
Stream<Integer> stream = Stream.of(nums);
stream.distinct()
.filter(value -> value > 1)
.forEach(integer -> System.out.println(integer));
}
}
②流的中间操作
StreamDemo02
package com.tzc;
import com.tzc.entity.Author;
import java.util.Arrays;
import java.util.List;
public class StreamDemo02 {
public static void main(String[] args) {
List<Author> authors = Author.getAuthors();
//案例3:中间操作map——对流中的元素进行计算或类型转换
authors.stream()
//将流中的元素类型由Author转换为Integer
.map(author -> author.getAge())
//令流中的元素都加10
.map(age -> age + 10)
.forEach(integer -> System.out.println(integer));
//案例4:中间操作sored——对流中的元素进行排序
//如果调用sorted()空参方法的话,则流中的元素类型必须实现Comparable<T>接口,对compareTo()方法进行重写,设定排序规则
authors.stream()
.sorted()
.forEach(author -> System.out.println(author.getAge()));
//也可以向sorted()方法传入匿名构造器,在其中对compareTo()方法进行重写
authors.stream()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.forEach(author -> System.out.println(author.getAge()));
//案例5:中间操作limited——设置流的最大长度,保留前n个元素,超过此长度的元素将被丢弃
authors.stream()
.sorted()
.limit(2)
.forEach(author -> System.out.println(author.getName()));
//案例6:中间操作skip——去掉流中的前n个元素,保留剩余元素
authors.stream()
.sorted()
.skip(2)
.forEach(author -> System.out.println(author.getName()));
//案例7:中间操作flatMap——map只能把一个对象转换为另一个对象来作为流的元素,
// 而flatMap可以将一个对象转换为多个对象来作为流的元素(其实是将一个对象转换为包含多个对象的流,再将流中的多个元素合并到原来的流中)
//例1:打印所有作者的所有书籍的名字,并去重
authors.stream()
.flatMap(author -> author.getBookList().stream())
.distinct()
.forEach(book -> System.out.println(book));
//例2:打印现有书籍的所有分类,要求对分类进行去重,且不能以这样的格式打印:“哲学、爱情”,必须以单个分类的格式打印
authors.stream()
.flatMap(author -> author.getBookList().stream())
.distinct()
.flatMap(book -> Arrays.stream(book.getCategory().split("、")))
.distinct()
.forEach(s -> System.out.println(s));
}
}
项目中见到的对中间操作map()和终结操作toList()的使用——只截取了stream流部分的代码,不可运行
//出自package io.github.xxyopen.novel.manager.cache.BookRankCacheManager;
//bookInfoMapper.selectList(bookInfoQueryWrapper)返回值为List<BookInfo>
return bookInfoMapper.selectList(bookInfoQueryWrapper).stream().map(v -> {
BookRankRespDto respDto = new BookRankRespDto();
respDto.setId(v.getId());
respDto.setCategoryId(v.getCategoryId());
respDto.setCategoryName(v.getCategoryName());
respDto.setBookName(v.getBookName());
respDto.setAuthorName(v.getAuthorName());
respDto.setPicUrl(v.getPicUrl());
respDto.setBookDesc(v.getBookDesc());
respDto.setLastChapterName(v.getLastChapterName());
respDto.setLastChapterUpdateTime(v.getLastChapterUpdateTime());
respDto.setWordCount(v.getWordCount());
return respDto;
}).toList();
③流的终结操作
StreamDemo03
package com.tzc;
import com.tzc.entity.Author;
import com.tzc.entity.Book;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class StreamDemo03 {
public static void main(String[] args) {
List<Author> authors = Author.getAuthors();
//案例8:终结操作count——返回当前流中元素的数量
long count = authors.stream()
.count();
System.out.println(count);
//案例9:终结操作max/min——返回当前流中元素的最大值/最小值
//需要向max/min()方法传入匿名构造器,在其中对compareTo()方法进行重写,规定排序规则,这样才能找出最大/小值
//返回值为Optional数据类型,后面再详细介绍
//例1:找出全部书籍的评分的最高分和最低分
Optional<Double> max = authors.stream()
.flatMap(author -> author.getBookList().stream())
.map(book -> book.getScore())
.max((o1, o2) -> (int) (o1 - o2));
System.out.println(max.get());
Optional<Double> min = authors.stream()
.flatMap(author -> author.getBookList().stream())
.map(book -> book.getScore())
.min((o1, o2) -> (int) (o1 - o2));
System.out.println(min.get());
//案例10:终结操作collect——将当前流转换为集合
//例1:获取一个存放所有作者名字的List集合——collect(Collectors.toList()),或是直接使用toList()亦可
List<String> list = authors.stream()
.map(author -> author.getName())
//.collect(Collectors.toList());
.toList();
System.out.println(list);
//例2:获取一个存放所有作者名字的Set集合——collect(Collectors.toSet())
Set<String> set = authors.stream()
.map(author -> author.getName())
.collect(Collectors.toSet());
System.out.println(set);
//例3:获取一个Map集合,map的key为作者名,value为List<Book>
//转换为List和Set都无需传参,但转换为Map需要传入两个参数,分别指定key和value,注意这里的key值不能有重复,否则会报错
Map<String, List<Book>> map = authors.stream()
.distinct()
.collect(Collectors.toMap(author -> author.getName(), author -> author.getBookList()));
System.out.println(map);
//案例11:终结操作anyMatch——判断流中是否有任意一个元素满足判断条件
//例1:判断是否存在年龄大于29的作家
boolean match1 = authors.stream()
.anyMatch(author -> author.getAge() > 29);
System.out.println(match1);
//案例12:终结操作allMatch——判断流中是否有所有元素均满足判断条件
//例1:判断作家年龄是否都大于18
boolean match2 = authors.stream()
.allMatch(author -> author.getAge() > 18);
System.out.println(match2);
//案例13:终结操作noneMatch——判断流中是否有所有元素均不满足判断条件
//例1:判断作家年龄是否都小于100
boolean match3 = authors.stream()
.noneMatch(author -> author.getAge() > 100);
System.out.println(match3);
//案例14:终结操作findAny——获取流中的任一元素,返回值为Optional类型
//例1:获取任意一个年龄大于18的作家,若存在则输出其名字
Optional<Author> optionalAuthor1 = authors.stream()
.filter(author -> author.getAge() > 18)
.findAny();
//若存在则输出,不存在则不输出,可以避免空指针异常
optionalAuthor1.ifPresent(author -> System.out.println(author));
//案例15:终结操作findFirst——获取流中的第一个元素
//例1:获取一个年龄最小的作家,并输出其名字
Optional<Author> optionalAuthor2 = authors.stream()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.findFirst();
optionalAuthor2.ifPresent(author -> System.out.println(author.getName()));
//案例16:终结操作reduce——对流中的数据按照指定的计算方式进行计算并返回结果
//双参数形式:第一个参数identity为结果,第二个参数则规定了对结果和流中的每个元素进行怎样的计算
//例1:使用reduce计算所有作者的年龄的和
Integer reduce = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(0, (result, element) -> result + element);
System.out.println(reduce);
//例2:使用reduce求所有作者的年龄的最大值
Integer reduce1 = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(-1, (result, element) -> (result > element) ? result : element);
System.out.println(reduce1);
//单参数形式:规定了对结果和流中的每个元素进行怎样的计算即可,默认从流中的第一个元素开始计算,此时返回值为Optional类型
//例2:使用reduce求所有作者的年龄的最大值
Optional<Integer> reduce2 = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce((result, element) -> (result > element) ? result : element);
reduce2.ifPresent(integer -> System.out.println(integer));
//三参数形式:待后续学习
}
}
4. 注意事项
-
惰性求值,如果没有终结操作是不会执行的
-
流是一次性的,经过终结操作之后就不能再被使用
-
不会影响元数据
四、Optional
1. 概述
2. 使用
①创建对象
-
optional就像是包装类,可以把我们的具体数据封装Optional对象内部, 然后我们去使用它内部封装好的方法操作封装进去的数据就可以很好的避免空指针异常
-
一般我们使用Optional.ofNullable来把数据封装成一个optional对象,无论传入的参数是否为null都不会出现问题——推荐使用
Author author = getAuthor(); Optional<Author> author = Optional.ofNullable(author);
-
如果你确定一个对象不是空的话就可以用Optional.of这个静态方法来把数据封装成Optional对象
-
Optional.of(author);
这里一定不能是null值传入,可以试试会出现空指针
-
-
如果返回的是null,这时可以使用Optional.empty()来进行封装
②安全消费值
-
当我们获取到一个Optional对象的时候,可以用ifPresent方法来去消费其中的值, 这个方法会先去判断是否为空,不为空才会去执行消费代码,优雅避免空指针
-
OptionalObject.ifPresent()
-
③安全获取值
-
orElseGet:获取数据并且设置数据为空时的默认值,如果数据不为空就获取该数据,为空则获取默认值
-
orElseThrow
④过滤
-
我们可以使用filter方法对数据进行过滤,如果原来是有数据的,但是不符合判断,也会变成一个无数据的Optional对象
-
Optional.filter()
⑤判断
-
Optional.isPresent() 判断数据是否存在,空则返回false,否则true,这种方式不是最好的,推荐使用Optional.ifPresent()
-
Optional.ifPresent(),上面isPresent不能体现Optional的优点
-
使用的时候可以先判断,相当于先判空,再去get,这样就不会空指针了
⑥数据转换
-
Optional还提供map可以对数据进行转换,并且转换得到的数据还是Optional包装好的,保证安全使用
OptionalDemo
package com.tzc;
import com.tzc.entity.Author;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class OptionalDemo {
public static void main(String[] args) {
//创建对象:Optional.ofNullable()
Author author = Author.getAuthors().get(0);
Optional<Author> optional = Optional.ofNullable(author);
//安全消费值:ifPresent()
optional.ifPresent(author1 -> System.out.println(author1));
//安全获取值
//方式一:orElseGet()——若不为空则正常返回值,若为空则返回一个默认值
Author author1 = optional.orElseGet(() -> new Author());
//方式二:orElseThrow()——若不为空则正常返回值,若为空则抛出一个异常(这里要用try-catch包住)
try {
Author author2 = optional.orElseThrow(() -> new RuntimeException("值为空"));
} catch (Throwable e) {
throw new RuntimeException(e);
}
//过滤:filter()
optional.filter(author3 -> author3.getAge() > 18)
.ifPresent(author4 -> System.out.println(author4));
//判断:isPresent()
boolean present = optional.isPresent();
System.out.println(present);
//数据转换:map()
optional.map(author5 -> author5.getBookList())
.ifPresent(books -> System.out.println(books));
}
}
五、函数式接口
1. 概述
-
只有一个抽象方法的接口就是函数式接口
-
JDK的函数式接口都加上了@FunctionalInterface注解进行标识,但是无论加不加该注解,只要接口中只有一个抽象方法,都是函数式接口
-
常见的函数式接口
-
Consumer 消费接口:可以对传入的参数进行消费
-
Function 计算转换接口:根据其中抽象方法的参数列表和返回值类型可以看到,可以在方法中对传入的参数计算或转换,把结果返回
-
Predicate 判断接口:可以在方法对传入的参数条件进行判断,返回判断结果
-
Supplier 生产型接口:可以在方法中创建对象,把创建好的对象返回
-
常用的默认方法
-
and :我们在使用Predicate接口的时候可能需要进行判断条件的拼接,而and方法相当于使用&&来拼接两个判断条件
-
or
六、方法引用——语法糖
-
我们在使用lambda时,如果方法体中只有一个方法的时候,包括构造方法,我们可以用方法引用进一步简化代码
1.用法及基本格式
-
方法体中只有一个方法时
-
类名或者对象名::方法名
2. 语法——了解即可
-
6.2.1 引用类静态方法 类名::方法名 使用前提:如果我们在重写方法的时候,方法体中只有一行代码, 并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有参数都按照顺序传入了这个静态方法中, 这个时候我们就可以引用类的静态方法。
-
6.2.2 引用对象的实例方法 对象名::方法名 使用前提:如果我们在重写方法的时候,方法体只有一行代码,并且这行代码是调用了某个对象的成员方法, 并且我们把要重写的抽象方法里面中所有的参数都按照顺序传入了这个成员方法(就是类的方法)中,这个时候我们就可以引用对象的实例方法。
-
6.2.3 引用类的实例方法 类名::方法名 使用前提:如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法, 并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。
-
6.2.4 构造器引用 类名::new StringBuilder::new
七、高级用法
基本数据类型优化:很多stream方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型,即使我们操作的是 整数小数,实际使用还是他们的包装类,JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便, 但是你一定要知道装箱拆箱也是需要一定的时间的,虽然这个时间消耗很小,但是在大量数据的不断重复的情况下,就不能忽视这个时间损耗了, stream对这块内容进行了优化,提供很多针对基本数据类型的方法。 例如:mapToInt,mapToLong,mapToDouble,flatMapToInt.... 比如前面我们用的map(),返回的是Stream<Integer>,如果你用.mapToInt(),最后返回的就是int值
PrimeDemo
package com.tzc;
import com.tzc.entity.Author;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
public class PrimeDemo {
public static void main(String[] args) {
List<Author> authors = Author.getAuthors();
//原始写法:因为存在大量计算,所以会有大量的自动装箱拆箱操作,这里不如直接将流中的元素类型转换为int类型
authors.stream()
.map(author -> author.getAge())
.map(age -> age + 10)
.filter(integer -> integer > 10)
.map(integer -> integer + 2)
.forEach(System.out::println);
//直接使用mapToInt()将流中的元素类型转换为基础数据类型int,便于计算
authors.stream()
.mapToInt(value -> value.getAge())
.map(age -> age + 10)
.filter(integer -> integer > 10)
.map(integer -> integer + 2)
.forEach(System.out::println);
}
}
八、并行流——parallel()、parallelStream()
当流中有大量元素时,我们可以使用并行流去提高操作的效率,其实并行流就是把任务分配给多个线程去完成,如果我们自己去用代码取实现的话
ParallelDemo
package com.tzc;
import com.tzc.entity.Author;
import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.stream.Stream;
public class ParallelDemo {
public static void main(String[] args) {
//将串行流转换为并行流,提高处理速度(当存在大量数据时推荐使用)
//parallel()
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> reduce = stream.parallel()
//查看处理每个Integer的线程的情况
.peek(x -> System.out.println(x + Thread.currentThread().getName()))
.filter(integer -> integer > 4)
.reduce(Integer::sum);
reduce.ifPresent(System.out::println);
//parallelStream()
List<Author> authors = Author.getAuthors();
authors.parallelStream()
.map(author -> author.getAge())
//查看处理每个Integer的线程的情况
.peek(integer -> System.out.println(integer + Thread.currentThread().getName()))
.map(age -> age + 10)
.filter(integer -> integer > 10)
.map(integer -> integer + 2)
.forEach(System.out::println);
}
}