List strings = Arrays.asList("abc","","bc","efg","abcd","","jkl");
//过滤流里元素""并把过滤后的stream转成集合
List collects = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
2.为数组生成流对象
String[] names={"a","b","c"};
Stream stream = Arrays.stream(names);
3.直接将几个值变成流对象 Stream.of()
Stream abc = Stream.of("abc", "hha", "hehh");
4.文件变成流
Stream lines = Files.lines(Paths.get("文件路径"), Charset.defaultCharset());
5, 通过 Collection#stream 方法或 List#stream 方法或 Set#stream 方法用集合创建 Stream 对象:
Stream stream = collection.stream();
Stream stream = list.stream();
Stream stream = set.stream();
6,通过 Stream.generate(Supplier) 方法配合 Stream#limit 方法直接创建 Stream 对象
例如:
Stream stream = Stream.generate(Math::random).limit(10);
逻辑上,因为通过 Stream.generate 方法生成的 Stream 对象中的数据的数量是无限的(即,你向 Stream 对象每次『要』一个对象时它都会每次生成一个返回给你,从而达到『无限个』的效果),所以,会结合 Stream#limit 方法来限定 stream 流中的数据总量。
7:通过 Stream.iterator(Final T, final UnaryOperator
Stream stream = Stream.iterate(1, n -> n += 2);
// 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ...
Stream.iterator 方法要求你提供 2 个参数:
数据序列中的第一个数。这个数字需要使用者人为指定。通常也被称为『种子』。
根据『前一个数字』计算『下一个数字』的计算规则。
整个序列的值就是:x, f(x), f(f(x)), f(f(f(x))), ...
逻辑上,因为通过 Stream.iterator 方法生成的 Stream 对象中的数据的数量是无限的
(即,你向 Stream 对象每次『要』一个对象时它都会每次生成一个返回给你,
从而达到『无限个』的效果),
所以,会结合 Stream#limit 方法来限定 stream 流中的数据总量。
jdk8集合是怎么遍历的:
List strings = Arrays.asList("abc","","bc","efg","abcd","","jkl");
List collects = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("遍历集合:");
//lamdba方式
collects.forEach(collect-> { System.out.println(collect+" ");});
//方法引用方式
collects.forEach(System.out::println);
遍历流:
void forEach(Consumer action)
;该方法接受一个Consumer
函数式接口,可以传递lamdba
表达式,进行消费数据
List strings = Arrays.asList("abc","","bc","efg","abcd","","jkl");
System.out.println("遍历流:");
strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::print);
用于映射每个元素到对应的结果 。
; Function
可以将一种T
类型转换成R
类型,这种转换动作,称为映射。
List numbers=Arrays.asList(1,2,3,4);
List collect = numbers.stream().map(number -> {
number++;
number = number * 2;
return number;
}).collect(Collectors.toList());
collect.forEach(System.out::println);
用于通过设置的条件过滤出元素, Stream
;
Stream.of("张三","李四","王五").filter(string ->string.startsWith("张")).forEach(System.out::print);
多条件:也可以使用多个filer
List<KfCollectPO> collect = kfCollectPOS.stream()
.filter(kfCollectPO -> resourceTypeSet.equals(kfCollectPO.getResourceType()) && kfCollectPO.getResourceId() == kfDataSetPO.getSetId())
.collect(Collectors.toList());
Stream流属于管道流,只能被消费一次。第一个流调用完毕就会流转到下一个流上,而这时第一个流已经使用完毕,会自动关闭。
Stream<String> stringStream= Stream.of("张三", "李四", "王五");
Stream<String> stringStream1 = stringStream.filter(string -> string.startsWith("张"));
stringStream1.forEach(System.out::print);
//第一个流已经关闭,在使用会报错:java.lang.IllegalStateException: stream has already been operated upon or closed
stringStream.forEach(System.out::print);
用于统计Stream
中元素的个数
Stream<String> stringStream= Stream.of("张三", "李四", "王五");
long count = stringStream.count();
System.out.println(count);
用于获取指定数量的流
Stream stringStream= Stream.of("张三", "李四", "王五");
stringStream.limit(2).forEach(System.out::print);
Stream stringStream= Stream.of("张三", "李四", "王五");
stringStream.skip(2).forEach(System.out::print);
int pageSize=3;
int pageNo=2;
Stream<String> stringStream= Stream.of("张三", "李四", "王五","李六","逍遥子","杨过");
stringStream.skip(pageSize * (pageNo-1)).limit(pageSize).forEach(System.out::print);
用于将两个流合并成一个流
Stream<String> stringStream1= Stream.of("张三", "李四", "王五");
Stream<String> stringStream2= Stream.of("李六","逍遥子","杨过");
Stream.concat(stringStream1,stringStream2 ).forEach(System.out::print);
用于对流进行排序
List str4=Arrays.asList(4,3,2,123);
str4.stream().sorted().forEach(System.out::println);
//倒叙
List str4=Arrays.asList(4,3,2,123);
str4.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
自定义排序规则:
//按照abcd排序
Stream.of("b","a","d","c","e","f").sorted(Comparator.comparing(String::hashCode)).forEach(System.out::print);
//倒序
Stream.of("b","a","d","c","e","f").sorted(Comparator.comparing(String::hashCode).reversed()).forEach(System.out::print);
//对集合排序,排序规则是元素的名字字段
list.stream().sorted(Comparator.comparing(Student::getAge));
comparing
方法入参为Function
函数式接口,说明可以传一个lamdba
表达式,可以使用方法引用优化,。
底层实际上使用的是compareTo
方法进行比较,因为Function
的出参继承自U
,而U
继承自Comparable
接口,所以能使用compareTo
方法
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
List str3 = Arrays.asList("abc", "", "abc", "efg", "abcd","", "jkl");
List collect = str3.stream().distinct().collect(Collectors.toList());
collect.forEach(System.out::println);
flatMap
参数:Function
;
List list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list.stream().map(s -> s.split(" ")).flatMap(string->{
Stream stream = Arrays.stream(string);
return stream;
}).forEach(System.out::print);
分析:map
把集合中的每一个元素按照" "分割并返回一个String
数组,map
完成后,流的类型变成Stream
,flatMap
先使用Array.stream
将传进来的每个String[]
转成Stream
流,然后就将这些流连接成一个流并返回得到一个完整的Stream
。
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
//生成Stream<String[]>,里面包含三个String[]
//[aaa, bbb, ccc]
//[ddd, eee, fff]
//[ggg, hhh, iii]
Stream<String[]> stream1 = list.stream().map(s -> s.split(" "));
//flatMap遍历生成Stream,每个元素都是一个String[]
//里面的逻辑是,把数组转成Stream<String>
//然后通过flatMap的遍历,把三个String[]生成的Stream<String>,都拼成一个Stream<String>
stream1.flatMap(string->{
//Arrays.stream可以为数组输出成流对象
Stream<String> stream = Arrays.stream(string);
return stream;
}).forEach(System.out::print);
流中是否至少有一个元素匹配给定的 T -> boolean
条件,有一个或多个元素满足条件,返回true
,没有一个元素满足条件,返回false
String[] words=new String[]{"hello","world"};
boolean b = Arrays.stream(words).anyMatch(word -> {
if ("hello".equals(word))
return true;
return false;
});
System.out.println(b);
true
流中是否所有元素都匹配给定的 T -> boolean
条件,全都匹配,返回true
,否则,返回false
String[] words=new String[]{"hello","world"};
boolean b = Arrays.stream(words).allMatch(word -> {
if ("hello".equals(word))
return true;
return false;
});
System.out.println(b);
false
String[] words=new String[]{"hello","hello"};
boolean b = Arrays.stream(words).allMatch(word -> {
if ("hello".equals(word))
return true;
return false;
});
System.out.println(b);
true
流中是否没有元素匹配给定的 T -> boolean
条件,没有一个元素匹配,返回true
,有一个元素匹配,返回fasle
。
String[] words=new String[]{"hello","world"};
boolean b = Arrays.stream(words).noneMatch(word ->"123".equals(word));
System.out.println(b);
true
String[] words=new String[]{"hello","world"};
boolean b = Arrays.stream(words).noneMatch(word ->"hello".equals(word));
System.out.println(b);
false
找到其中一个元素 (使用 stream()
时找到的是第一个元素;使用 parallelStream()
并行时找到的是其中一个元素)
返回一个Optional
类型的元素
Optional介绍
List str3 = Arrays.asList("abc", "", "abc", "efg", "abcd","", "jkl");
Optional optionalS = str3.stream().findAny();
System.out.println(optionalS.get());
Optional any = str3.parallelStream().findAny();
System.out.println(any.get());
abc
abcd
找到第一个元素
List str3 = Arrays.asList("abc", "", "abc", "efg", "abcd","", "jkl");
Optional optionalS = str3.stream().findFirst();
System.out.println(optionalS.get());
Optional any = str3.parallelStream().findFirst();
System.out.println(any.get());
abc
abc
reduce
函数接收两个参数:1.初始值 2.进行归约操作的Lambda
表达式
自定义Lambda
表达式实现规约操作,如果当前流的元素为数值类型,那么可以使用Integer
提供的sum
函数代替自定义的Lambda
表达式,Integer
类还提供了min
、max
等一系列数值操作,当流中元素为数值类型时可以直接使用。
List list=new ArrayList<>();
list.add(new Person("张三",24));
list.add(new Person("李四",26));
list.add(new Person("王五",28));
//求和 0表示初始值
int sum1 = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
int sum2 = list.stream().map(Person::getAge).reduce(0, Integer::sum);
System.out.println("和:"+sum1);
System.out.println("和:"+sum2);
//求积
Integer reduce = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
System.out.println("积:"+reduce);
//最大值
Optional reduce1 = list.stream().map(Person::getAge).reduce(Integer::max);
System.out.println("最大值:"+reduce1.get());
reduce
的第一个参数表示初试值
reduce
的第二个参数为需要进行的归约操作,它接收一个拥有两个参数的Lambda
表达式,reduce
会把流中的元素两两输给Lambda
表达式,最后将计算出来。
reduce()第一个参数是上次累计的和,第二个参数是数据流的下一个元素
采用reduce
进行数值操作会涉及到基本数值类型和引用数值类型之间的装箱、拆箱操作,因此效率较低。 当流操作为纯数值操作时,使用数值流能获得较高的效率。
StreamAPI
提供了三种数值流:IntStream、DoubleStream、LongStream
将普通流转换成数值流的三种方法:mapToInt、mapToDouble、mapToLong
数值流转成普通流:boxed()
;
将串行数值流转成并行数值流:parallel()
每种数值流都提供了数值计算函数,如max、min、sum、average
等。
OptionalDouble average = list.stream().map(Person::getAge).mapToInt(Integer::intValue).average();
System.out.println(average.getAsDouble());
max,min
返回结果为OptionalInt,sum
为int
;Double,Long
类似
IntStream
与 LongStream
拥有 range
和 rangeClosed
方法用于数值范围处理。
IntStream
: rangeClosed(int, int) / range(int, int)
LongStream
: rangeClosed(long, long) / range(long, long)
这两个方法的区别在于一个是闭区间,一个是半开半闭区间:
rangeClosed(1, 100)
:[1, 100]
range(1, 100)
:[1, 100)
long sum = LongStream.rangeClosed(0L, 100).sum();
collect
方法作为终端操作,接受的是一个 Collector
接口参数,能对数据进行一些收集归总操作
最常用的方法,把流中所有元素收集到一个 List
, Set
或 Collection
中
public static void main(String[] args) {
List<Person> data = new ArrayList<>();
Person p1=new Person("张三",1);
Person p2=new Person("李四",2);
Person p3=new Person("王五",3);
data.add(p1);
data.add(p2);
data.add(p3);
//toMap 第一个参数为key 第二个参数为value
Map<String, Integer> collect = data.stream().collect(Collectors.toMap(Person::getName, Person::getAge));
// Map collect = data.stream().collect(Collectors.toMap(item->item.getName(), item->item.getAge()));
System.out.println(collect);
// {李四=2, 张三=1, 王五=3}
// {李四=2, 张三=1, 王五=3}
// {李四=2, 张三=1, 王五=3}
}
@AllArgsConstructor
@Data
static class Person{
private String name;
private int age;
}
用于计算数据的数量:
List numbers=Arrays.asList(1,2,3,4);
Long collect = numbers.stream().collect(Collectors.counting());
System.out.println(collect);
4
//也可以不使用收集器的计数器
long count = numbers.stream().count();
推荐第二种
summing
,没错,也是计算总和,不过这里需要一个函数参数
List<Person> list=new ArrayList<>();
list.add(new Person("张三",24));
list.add(new Person("李四",26));
list.add(new Person("王五",28));
//使用收集器的规约操作
Integer collect = list.stream().collect(Collectors.summingInt(Person::getAge));
System.out.println(collect);
//使用reduce的规约操作
Integer reduce = list.stream().map(Person::getAge).reduce(0, Integer::sum);
System.out.println(reduce);
//使用数值流的规约操作
int sum = list.stream().mapToInt(Person::getAge).sum();
System.out.println(sum);
三种规约操作,函数式编程通常提供了多种方式来完成同一种操作,推荐使用数值流。
List<Person> list=new ArrayList<>();
list.add(new Person("张三",24));
list.add(new Person("李四",26));
list.add(new Person("王五",28));
//使用收集器的规约操作
Double collect = list.stream().collect(Collectors.averagingInt(Person::getAge));
System.out.println(collect);
//使用数值流的规约操作
double asDouble = list.stream().mapToInt(Person::getAge).average().getAsDouble();
System.out.println(asDouble);
这三个方法比较特殊,比如 summarizingInt
会返回 IntSummaryStatistics
类型
IntSummaryStatistics collect = list.stream().collect(Collectors.summarizingInt(Person::getAge));
System.out.println("最大值:"+collect.getMax());
System.out.println("最小值:"+collect.getMin());
System.out.println("和:"+collect.getSum());
System.out.println("平均值:"+collect.getAverage());
System.out.println("总数:"+collect.getCount());
IntSummaryStatistics
包含了计算出来的平均值,总数,总和,最值。
maxBy,minBy
两个方法,需要一个 Comparator
接口作为参数
Optional<Person> collect = list.stream().collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));
System.out.println(collect.get().getAge());
//也可以直接使用 max 方法获得同样的结果
Optional<Person> max = list.stream().max(Comparator.comparing(Person::getAge));
System.out.println(max.get().getAge());
joining
连接字符串对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder
。
String collect = list.stream().map(Person::getName).collect(Collectors.joining());
System.out.println(collect);
结果:张三李四王五
String collect = list.stream().map(Person::getName).collect(Collectors.joining(":"));
System.out.println(collect);
结果:张三:李四:王五
//第一个参数是连接符,第二个参数是前缀,第三个参数是后缀
String collect = list.stream().map(Person::getName).collect(Collectors.joining(":","开始:"," 结束"));
System.out.println(collect);
开始:张三:李四:王五 结束
若你需要自定义一个归约操作,那么需要使用Collectors.reducing
函数,该函数接收三个参数:
例子一:
List numbers=Arrays.asList(1,2,3,4);
Integer collect = numbers.stream().collect(Collectors.reducing(0, Integer::intValue, (a, b) -> a + b));
System.out.println(collect);
例子二:
Optional sumAge = list.stream()
.collect(Collectors.reducing(0,Person::getAge,(i,j)->i+j));
第三个参数表示归约的过程。这个参数接收一个Lambda
表达式,而且这个Lambda
表达式一定拥有两个参数,分别表示当前相邻的两个元素。由于我们需要累加,因此我们只需将相邻的两个元素加起来即可。
groupingBy
分组https://blog.csdn.net/zhouzhiwengang/article/details/112319054
分组就是将流中的元素按照指定类别进行划分,类似于SQL
语句中的GROUPBY
。
Map<Integer, List<Person>> collect = list.stream().collect(Collectors.groupingBy(Person::getAge));
collect.forEach((k,v)->{
System.out.println(k);
v.stream().forEach(a->{
System.out.println(a.getName()+" "+a.getAge());
});
});
Map> collect = teachers.stream().collect(Collectors.groupingBy(teacher -> {
if (teacher.getAge() > 60)
return "老年老师";
else if (teacher.getAge() > 40)
return "中年老师";
else
return "青年老师";
}));
//遍历map
collect.forEach((k,v)-> {
System.out.println(k);
v.forEach(teacher -> System.out.println(teacher.getId()+" "+teacher.getName()+" "+teacher.getAge()));
});
groupingby
函数接收一个Lambda
表达式,该表达式返回String
类型的字符串,groupingby
会将当前流中的元素按照Lambda
返回的字符串进行分组。
分组结果是一个Map< String,List< Person>>,Map
的键就是组名,Map
的值就是该组的Perosn
集合。
多级分组可以支持在完成一次分组后,分别对每个小组再进行分组。
使用具有两个参数的groupingby
重载方法即可实现多级分组。
第一个参数:一级分组的条件
第二个参数:一个新的groupingby
函数,该函数包含二级分组的条件
//多级分组 先通过年龄分组,再通过性别分组
Map>> collect = teachers.stream()
.collect(groupingBy(teacher -> {
if (teacher.getAge() > 60)
return "老年老师";
else if (teacher.getAge() > 40)
return "中年老师";
else
return "青年老师";
}, groupingBy(Teacher::getSex)));
//遍历map
collect.forEach((k,v)-> {
System.out.println(k);
v.forEach((a,b) -> {
System.out.println(a);
b.forEach(teacher -> System.out.println(teacher.getName()+" "+teacher.getAge()+" "+teacher.getSex()));
});
System.out.println("-----------------------------------------------------------------");
});
Map collect = list.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
collect.forEach((k,v)->{
System.out.println(k+" "+v);
});
此时会返回一个Map< Integer,Long>
类型的map
,该map
的键为组名,map
的值为该组的元素个数。
分区与分组的区别在于,分区是按照 true
和 false
来分的,因此partitioningBy
接受的参数的 lambda
也是 T -> boolean
Map<Boolean, List<Person>> collect = list.stream()
.collect(Collectors.partitioningBy(person -> person.getAge() <= 26));
System.out.println(collect.toString());
{false=[Person{name='王五', age=28}, Person{name='小李子', age=28}],
true=[Person{name='张三', age=24}, Person{name='李四', age=26}]}
同样地 partitioningBy
也可以添加一个收集器作为第二参数,进行类似 groupBy
的多重分区等等操作。
并行流就是把内容分成多个数据块,使用不同的线程分别处理每个数据块的流。这也是流的一大特点,要知道,在 Java 7 之前,并行处理数据集合是非常麻烦的,你得自己去将数据分割开,自己去分配线程,必要时还要确保同步避免竞争。不是所有情况的适合,有些时候并行甚至比顺序进行效率更低,而有时候因为线程安全问题,还可能导致数据的处理错误。
普通的并行流
//lists.parallelStream()
long start = System.currentTimeMillis();
//串行数值流转成并行数值流 261 262 256
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
//只用数值流 642 702 676
//long sum = LongStream.rangeClosed(0L, 10_0000_0000L).reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间:"+(end-start));
复现问题:筛选出集合integers
中取余2为0的元素并放到一个新集合中
结果:并行流出现线程安全问题:要么ArrayIndexOutOfBoundsException
,要么strings.size()<500
List<Integer> integers = new ArrayList<>();
for (int i = 0; i < 1000; i++ ) {
integers.add(i);
}
List<String> strings = new ArrayList<>();
integers.parallelStream().filter(integer -> integer%2==0).forEach(i -> strings.add(i.toString()));
int count=0;
for (String string : strings) {
System.out.println(string);
count++;
}
System.out.println("数量:"+count);
List<String> strings = new Vector<>(1000);
integers.parallelStream().filter(integer -> integer%2==0).forEach(i -> strings.add(i.toString()));
int count=0;
for (String string : strings) {
System.out.println(string);
count++;
}
System.out.println("数量:"+count);
解决方式2:使用集合工具提供的Collections.synchronizedList(new ArrayList<>())
List<String> strings = Collections.synchronizedList(new ArrayList<>());
integers.parallelStream().filter(integer -> integer%2==0).forEach(i -> strings.add(i.toString()));
int count=0;
for (String string : strings) {
System.out.println(string);
count++;
}
System.out.println("数量:"+count);
解决方式3:使用并发包下的CopyOnWriteArrayList
List<String> strings = new CopyOnWriteArrayList<>();
integers.parallelStream().filter(integer -> integer%2==0).forEach(i -> strings.add(i.toString()));
int count=0;
for (String string : strings) {
System.out.println(string);
count++;
}
System.out.println("数量:"+count);
上面三种方式,底层都使用了锁,前两种使用的synchronized
, CopyOnWriteArrayList
使用的lock
锁,用了锁性能肯定会有所下降。
并行流的正确的使用方式:
List<Integer> collect = integers
.parallelStream()
.filter(integer -> integer % 2 == 0)
.collect(Collectors.toList());
int count=0;
for (Integer integer : collect) {
System.out.println(integer);
count++;
}
System.out.println("数量:"+count);
不但没有出现异常和数量不一致的问题,而且还排序,在采用并行流收集元素到集合中时,最好调用collect
方法,一定不要采用Foreach
方法或者map
方法
// 获取当前建筑的所有楼层
List<String> allFloors = getAllFloors(workbenchStatisticsAO);
// 获取所有楼层的设备
List<DeviceVO> devices = allFloors.stream().parallel().map(id -> {
// 不能放外面,放外面就是共享资源,并行执行的时候,会存在线程安全问题,因为这个共享资源会被修改,所以存在线程安全问题,如果只是读,是不会存在线程安全问题的
GetDevicesAO getDevicesAO = BeanUtil.copyProperties(workbenchStatisticsAO, GetDevicesAO.class);
// 获取当前楼层的设备
getDevicesAO.setFloorId(id);
List<DeviceVO> deviceVOList = deviceService.chillerList(getDevicesAO);
return deviceVOList;
}).flatMap(Collection::stream).collect(Collectors.toList());
main
函数启动java
应用,按道理只有两条线程,一条main
线程,一条GC
线程。能参与并行流的线程只有main
线程,如果只有main
线程一条线程,那怎么称的上是并行呢?
public static void main(String[] args) {
List<Double> doubles = new ArrayList<>();
for (double i = 0; i < 10; i++ ) {
doubles.add(i);
}
//i=10000 i=100000
//串行:4999.5 time:75 49999.5 time:86
//并行:4999.5 time:76 49999.5 time:96
long start = System.currentTimeMillis();
double collect = doubles
.stream()
.mapToDouble(Double::doubleValue)
.parallel()//加上就是并行数值流,注释就是串行数值流
.peek(a-> System.out.println(Thread.currentThread().getName()))//查看线程
.average()
.getAsDouble();
long end = System.currentTimeMillis();
System.out.println(collect+" time:"+(end-start));
}
结果:
ForkJoinPool.commonPool-worker-3
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-7
main
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-7
4.5 time:77
经过测试查看,发现并行流底层使用的是并发包下的ForkJoin
https://www.jianshu.com/p/e429c517e9cb
https://www.jianshu.com/p/ac2bcf2f9d48
https://blog.csdn.net/qq_38974634/article/details/81347604