
原创/朱季谦
因笔者主要从事风控反欺诈相关工作,故而此文使用比较熟悉的三要素之一的【手机号】黑名单作代码案例说明。
在实际项目当中,若能熟练使用Java8 的Stream流特性进行开发,就比较容易写出简洁优雅的代码。目前市面上很多开源框架,如Mybatis- Plus、kafka Streams以及Flink流处理等,都有一个相似的地方,即用到Stream流特性,其写出的代码简洁而易懂,当然,若是在不熟悉流特性的基础上而贸然去使用Stream开发的话,难免会写出一手bug。
一、Stream中间操作
Stream的中间操作是指在流链当中,可以对数据进行处理操作,包括filter过滤、map映射转换、flatMap合并、distinct去重、sorted排序等操作。这些操作都会返回一个新的Stream流对象,可以通过链式调用多个中间操作进行复杂的数据处理。需要注意的是,中间操作需要具有终止操作才会触发。
1.1、filter:过滤出符合条件的元素。
filter(方法常用于实现数据过滤,即可以对集合、数组等数据源筛选出符合指定条件的元素,并返回一个新的流。
//将数组转换为一个字符串列表
List<String> numbers = Arrays.asList("13378520000","13278520000","13178520000","13358520000";
//通过stream(方法创建一个流,接着使用filter(方法过滤出前缀为“133”的元素,最终通过collect( 方法将结果收集到一个新列表中
List<String> filterdNumbers = numbers.stream(.filter(s -> s.startsWith("133".collect(Collectors.toList(;
System.out.println(filterdNumbers;
打印结果:[13378520000, 13358520000]
1.2、map:映射转换元素。
根据以下两个案例分别学习map(将元素转换为另一个元素以及提取元素其中的信息——
1.2.1、转换元素
List<String> numbers = Arrays.asList("13378520000","13278520000","13178520000","13558520000";
//通过stream(方法创建一个流,使用map(方法将每个字符串转换为截取前7位的字符,最后使用collect(方法将结果收集到一个新列表中
List<String> filterdNumbers = numbers.stream(.map(s -> s.substring(0,7.collect(Collectors.toList(;
System.out.println(filterdNumbers;
打印结果:[1337852, 1327852, 1317852, 1355852]
1.2.2、提取元素信息
假设有一个用户对象列表,我们需要提取其中每个对象的手机号,可以使用map(方法实现:
List<People> peopleList = Arrays.asList(
new People("王二","13378520000",
new People("李二","13278520000",
new People("张四","13178520000"
;
//通过stream(方法创建一个流,使用map(方法提取每个用户的手机号,最后使用collect(方法将结果收集到一个新列表中
List<String> tel = peopleList.stream(.map(People::getTel.collect(Collectors.toList(;
System.out.println(tel;
打印结果:[13378520000, 13278520000, 13178520000]
1.3、flatMap:将多个流合并为一个流。
1.3.1、实现多对多的映射
假设有两组余额列表A和B,需要将A组每个元素都与B组所有元素依次进行相加,可以使用flatMap实现该多对多的映射——
List<Integer> listA = Arrays.asList(1, 2, 3;
List<Integer> listB = Arrays.asList(4, 5, 6;
List<Integer> list = listA.stream(.flatMap(a -> listB.stream(.map(b -> a +b.collect(Collectors.toList(;
System.out.println(list;
打印结果: [5, 6, 7, 6, 7, 8, 7, 8, 9]
1.3.2、将多个列表合并成一个列表
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("13378520000", "13278520000",
Arrays.asList("13178520000", "13558520000",
Arrays.asList("15138510000", "15228310000"
;
List<String> flatMapList = listOfLists.stream(.flatMap(Collection::stream.collect(Collectors.toList(;
System.out.println(flatMapList;
打印结果:[13378520000, 13278520000, 13178520000, 13558520000, 15138510000, 15228310000]
1.4、distinct:去除重复的元素。
假设有一个包含重复手机号字符串的列表,可以使用distinct(去重操作——
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000";
List<String> disNumbers = numbers.stream(.distinct(.collect(Collectors.toList(;
System.out.println(disNumbers;
打印结果:[13378520000, 15138510000, 13178520000]
注意一点的是,distinct用于针对流作去重操作时,需要确定流中元素实现了equals(和hashCode(方法,因为这两个方法是判断两个对象是否相等的标准。
1.5、sorted:排序元素。
假设需要对一组People对象按照年龄排序,下面分别按照升序排序和降序排序——
1.5.1、升序排序
List<People> peopleList = Arrays.asList(
new People("王二",20,
new People("李二",30,
new People("张四",31
;
List<People> newpeopleList=peopleList.stream(.sorted(Comparator.comparing(People::getAge.collect(Collectors.toList(;
//打印结果
newpeopleList.stream(.forEach(System.out::println;
打印结果:
People{name='王二', age=20}
People{name='李二', age=30}
People{name='张四', age=31}
1.5.2、降序排序
通过reversed(方法进行逆序排序,也就是将升序排序进行倒序排序——
List<People> peopleList = Arrays.asList(
new People("王二",20,
new People("李二",30,
new People("张四",31
;
List<People> newpeopleList = peopleList.stream(.sorted(Comparator.comparing(People::getAge.reversed(.collect(Collectors.toList(;
//打印结果
newpeopleList.stream(.forEach(System.out::println;
打印结果:
People{name='张四', age=31}
People{name='李二', age=30}
People{name='王二', age=20}
1.6、peek:查看每个元素的信息,但不修改流中元素的状态。
List<String> telList = Arrays.asList("13378520000","13278520000","13178520000","13558520000";
telList.stream(.peek(t -> System.out.println(t
.map(t -> t.substring(0,3
.peek(t -> System.out.println(t
.collect(Collectors.toList(;
打印结果:
13378520000
133
13278520000
132
peek(方法和forEach很类似,都是可以用于遍历流中的元素,但是,两者之间存在较大的区别。主要一点是,forEach在流中是一个终止操作,一旦调用它,就意味着Stream流已经被处理完成,不能再进行任何操作,例如,无法在forEach之后针对流进行map、filter等操作,但peek方法可以,以上的案例可以看出,在第一次调用peek打印一个元素后,该元素还可以接着进行map操作,进行字符串的前三位截取。
1.7、limit 和 skip:截取流中的部分元素。
limit(和skip(都是用于截取Stream流中部分元素的方法,两者区别在于,limit(返回一个包含前n个元素的新流,skip(则返回一个丢弃前n个元素后剩余元素组成的新流。
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.print("取数组前5个元素:";
Arrays.stream(arr.limit(5.forEach(n -> System.out.print(n + " "; // 输出结果为:1 2 3 4 5
System.out.print("跳过前3个元素,取剩余数组元素:";
Arrays.stream(arr.skip(3.forEach(n -> System.out.print(n + " "; // 输出结果为:4 5 6 7 8 9 10
二、Stream终止操作
Stream的终止操作是指执行Stream流链中最后一个步骤,到这一步就会结束整个流处理。在Java8中,Stream终止操作包括forEach、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst和findAny等。这些终止操作都有返回值。需要注意一点是,如果没有执行终止操作的话,Stream流是不会触发执行的,例如,一个没有终止操作的peek(方法代码是不会执行进而打印——
list.stream(.peek(t -> System.out.println("ddd"
当加上终止操作话,例如加上collect,就会打印出“ddd”——
list.stream(.peek(t -> System.out.println("ddd".collect(Collectors.toList(;
下面按类别分别讲解各个终止操作的使用。
2.1、forEach:遍历流中的每个元素。
2.2、count:统计流中元素的数量。
count可以统计流中元素的数量并返回结果。
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000";
long count = numbers.stream(
.distinct(//去重
.count(;//统计去重后的手机号
System.out.println(count;
打印结果:3
2.3、reduce:将流中的所有元素归约成一个结果。
reduce(可以将流中的所有元素根据指定规则归约成一个结果,并将该结果返回。
Optional<T> result = stream.reduce(BinaryOperator<T> accumulator;
可见,reduce方法会返回一个Optional类型的值,表示归约后的结果,需要通过get(方法获取Optional里的值。
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000";
Optional result = numbers.stream(
.distinct( //去重
.reduce((a ,b -> a+","+b;//指定规则为,相临两个字符通过逗号“,”间隔
System.out.println(result.get(;
打印结果:13378520000,15138510000,13178520000
2.4、collect:将流中的元素收集到一个容器中,并返回该容器。
collect的作用是将流中的元素收集到一个新的容器中,返回该容器。打个比喻,它就像一个采摘水果的工人,负责将水果一个个采摘下来,然后放进一个篮子里,最后将篮子交给你。我在前面的案例当中,基本都有用到collect,例如前面2.1的filter过滤用法中的List
2.5、min 和 max:找出流中的最小值和最大值。
min和max用来查找流中的最小值和最大值。
List<People> peopleList = Arrays.asList(
new People("王二",20,
new People("李二",30,
new People("张四",31
;
//查找年龄最小的用户,若没有则返回一个null
People people = peopleList.stream(.min(Comparator.comparing(People::getAge.orElse(null;
System.out.println(people;
打印结果:People{name='王二', age=20}
max的用法类似,这里不做额外说明。
2.6、anyMatch、allMatch 和 noneMatch:判断流中是否存在满足指定条件的元素。
2.6.1、anyMatch
anyMatch用于判断,如果流中至少有一个元素满足给定条件,那么返回true,反之返回false,即 true||false为true这类的判断。
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15338510000";
boolean hasNum = numbers.stream(.anyMatch(n -> n.startsWith("153";
System.out.println(hasNum;
打印结果:true
2.6.2、allMatch
allMatch用于判断,流中的所有元素是否都满足给定条件,满足返回true,反之false,即true&&false为false这类判断。
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15338510000";
boolean hasNum = numbers.stream(.allMatch(n -> n.startsWith("153";
System.out.println(hasNum;
打印结果:false
2.6.2、noneMatch
noneMatch用于判断,如果流中没有任何元素满足给定的条件,返回true,如果流中有任意一个条件满足给定条件,返回false,类似!true为false的判断。
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "1238510000";
//numbers里没有前缀为“153”的手机号
boolean hasNum = numbers.stream(.noneMatch(n -> n.startsWith("153";
System.out.println(hasNum;
打印结果:true
这三个方法其实存在一定互相替代性,例如在3.6.1中,满足!anyMatch表示所有手机号都不为“153”前缀,才得到true,这不就是noneMatch,主要看在项目当中如何灵活应用。
2.7、findFirst 和 findAny:返回流中第一个或任意一个元素。
2.7.1、findFirst
假设需要对一批同手机号的黑名单用户按照时间戳降序排序,然后取出第一个即时间戳为最早的用户,就可以使用findFirst——
List<People> peopleList = Arrays.asList(
new People("王二","13178520000","20210409",
new People("李二","13178520000","20230401",
new People("张四","13178520000","20220509",
new People("赵六","13178520000","20220109"
;
/**
* 先按照时间升序排序,排序后的结果如下:
* People{name='王二', tel='13178520000', time='20210409'}
* People{name='赵六', tel='13178520000', time='20220109'}
* People{name='张四', tel='13178520000', time='20220509'}
* People{name='李二', tel='13178520000', time='20230401'}
*
*排序后,People{name='王二', tel='13178520000', time='20210409'}成了流中的第一个元素
*/
People people = peopleList.stream(.sorted(Comparator.comparing(People::getTime.findFirst(.orElse(null;
System.out.println(people;
打印结果:People{name='王二', tel='13178520000', time='20210409'}
2.7.2、findAny
findAny返回流中的任意一个元素,如果流为空,则通过Optional对象返回一个null。
//blackList是已经存在的黑名单列表
List<String> blackList = Arrays.asList("13378520000", "15138510000";
//新来的手机号列表
List<String> phoneNumber = Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000";
String blackPhone = phoneNumber.stream(
//过滤出phoneNumber有包含在blackList的手机号,这类手机号即为黑名单手机号。
.filter(phone -> blackList.contains(phone
//获取过滤确定为黑名单手机号的任意一个
.findAny(
//如果没有则返回一个null
.orElse(null;
System.out.println(blackPhone;
打印结果:13378520000
三、并行流
3.1、什么是并行流:并行流的概念和原理。
并行流是指通过将数据按照一定的方式划分成多个片段分别在多个处理器上并行执行,这就意味着,可能处理完成的数据顺序与原先排序好的数据情况是不一致的。主要是用在比较大的数据量处理情况,若数据量太少,效率并不比顺序流要高,因为底层其实就使用到了多线程的技术。
1、输入数据:并行流的初始数据一般是集合或者数组,例如Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000";
2、划分数据:将初始数据平均分成若干个子集,每个子集可以在不同的线程中独立进行处理,这个过程通常叫“分支”(Forking),默认情况下,Java8并行流使用到了ForkJoinPool框架,会将Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000"划分成更小的颗粒进行处理,可能会将该数组划分成以下三个子集:
[13378520000, 13178520000] [1238510000, 13338510000] [13299920000]
3、处理数据:针对划分好的子集并行进行相同的操作,例如包括过滤(filter、映射(map、去重(distinct等,这个过程通常叫“计算”(Computing),例如需要过滤为前缀包括“133”的字符集合,那么,各个子集,就会处理得到以下结果:
[13378520000] [13338510000] []
4、合并结果:将所有子集处理完成的结果进行汇总,得到最终结果。这个过程通常叫“合并”(Merging),结果就会合并如下:
[13378520000,13338510000]
5、返回结果:返回最终结果。
3.2、创建并行流:通过 parallel( 方法将串行流转换为并行流。
可以通过parallel(方法将顺序流转换为并行流,操作很简单,只需要在顺序流上调用parallel(即可。
List<String> numbers = Arrays.asList("13378360000","13278240000","13178590000","13558120000";
//通过stream(.parallel(方法创建一个并行流,使用map(方法将每个字符串转换为截取前7位的字符,最后使用collect(方法将结果收集到一个新列表中
List<String> filNums = numbers.stream(.parallel(.map(s -> s.substring(0,7.collect(Collectors.toList(;
System.out.println(filNums;
打印结果:[1337836, 1327824, 1317859, 1355812]
3.3、并行流的注意事项:并行流可能引发的线程安全,以及如何避免这些问题。
在使用并发流的过程中,可能会引发以下线程安全问题:并行流中的每个子集都在不同线程运行,可能会导致对共享状态的竞争和冲突。
使用无状态操作:在并行流处理过程尽量使用无状态操作,例如filter、map之类的,可以尽量避免线程安全和同步问题。
四、Optional
4.1、什么是 Optional:Optional 类型的作用和使用场景。
4.2、如何使用 Optional:如何使用 Optional 类型。
使用Optional类型主要目的是在数据可能为空的情况下,提供一种更安全、更优雅的处理方式。
4.2.1、ofNullable(和isPresent(方法
将一个可能为null的对象包装成Optional类型的对象,然后根据isPresent方法判断对象是否包含空值——
String str = null;
Optional<String> optStr = Optional.ofNullable(str;
if (optStr.isPresent({
System.out.println("Optional对象不为空";
}else {
System.out.println("Optional对象为空";
}
打印结果:Optional对象为空
4.2.2、get(方法
String str = null;
Optional<String> optStr = Optional.ofNullable(str;
if (optStr.isPresent({
System.out.println("Optional对象不为空";
}else {
System.out.println("Optional对象为空";
optStr.get(;
}
控制台打印结果:
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135
at com.zhu.fte.biz.test.StreamTest.main(StreamTest.java:144
Optional对象为空
4.2.4、orElse(方法
String str = null;
Optional<String> optStr = Optional.ofNullable(str;
if (optStr.isPresent({
System.out.println("Optional对象不为空";
}else {
System.out.println("Optional对象为空,返回默认值:" + optStr.orElse("null";
}
打印结果:Optional对象为空,返回默认值:null
当然,如果不为空的话,则能正常获取对象中的值——
String str = "测试";
Optional<String> optStr = Optional.ofNullable(str;
if (optStr.isPresent({
System.out.println("Optional对象不为空,返回值:" + optStr.orElse("null";
}else {
System.out.println("Optional对象为空,返回默认值:" + optStr.orElse("null";
}
打印结果:Optional对象不为空,返回值:测试
那么,问题来了,它是否能判断“ ”这类空格的字符串呢,我实验了一下,
String str = " ";
Optional<String> optStr = Optional.ofNullable(str;
if (optStr.isPresent({
System.out.println("Optional对象不为空,返回值:" + optStr.orElse("null";
}else {
System.out.println("Optional对象为空,返回默认值:" + optStr.orElse("null";
}
打印结果:Optional对象不为空,返回值:
可见,这类空字符串,在orElse判断当中,跟StringUtils.isEmpty(类似,都是把它当成非空字符串,但是StringUtils.isBlank(则判断为空字符串。
4.2.5、orElseGet(方法
4.3、Optional 和 null 的区别: Optional 类型与 null 值的异同。
ofNullable(、of(或其他方法来创建。而null值则只是一个空引用,没有任何实际的值。
String str = null;
//错误示范:直接调用str.length(方法会触发NullPointerException
//int length = str.length(
//通过Optional类型避免NullPointerException
Optional<String> optionalStr = Optional.ofNullable(str;
if (optionalStr.isPresent({//判断Optional对象是否都包含非空值
int length = optionalStr.get(.length(;
System.out.println("字符串长度为:" + length;
}else {
System.out.println("字符串为空!";
}
//使用map(方法对Optional对象进行转换时,确保返回对结果不为null
Optional<Integer> optionalLength = optionalStr.map(s -> s.length(;
System.out.println("字符串长度为:" + optionalLength.orElse(-1; // 使用orElse(方法提供默认值
五、扩展流处理
除里以上常用的流处理之外,Java8还新增了一些专门用来处理基本类型的流,例如IntStream、LongStream、DoubleStream等,其对应的Api接口基本与前面案例相似,读者可以自行研究。
以上,就是关于Java8流处理相关知识的总结,笔者水平有限,若存在有误的地方,还需帮忙指正。
最后,码字不易,欢迎关注、点赞、收藏,谢谢!