前言
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。想必大家对
Steam再熟悉不过了,但是说到Java中的Stream呢?今天给大家分享必备小知识是集合操作利器、代码简化杀手之流式编程。
流式操作
What
Stream 即意为 “流”,JDK8 中 Stream 使用的是函数式编程模式,可以被用来对集合进行链状流式的操作。
例如:
// 给每个用户涨 10000 工资
users.stream()
.peek(user -> user.setSalary(user.getSalary() + 10000))
.forEach(System.out::println);
这种代码风格就称之为 Stream 的流式操作,结合 Lambda 表达式,代码看起来很简洁且优雅 (^o^)/~
Why
-
现在很多程序都离不开集合这种常用的数据结构,但是在集合处理方面内置的方法并不能满足个性化需求,
Stream流上的函数式操作应运而生。 -
从上述举例中也能窥出一二,使用
Stream流式操作能够很高效地处理包含大量数据的集合,代替了传统的for或者增强 for循环的写法,并且使用链式编程使得代码更为简洁优雅。 -
Stream API使用了Fork/Join框架充分利用了自身机器的多核架构,让流管道以并行的方式运行,即并行流,极大优化处理性能,实现并发编程。
How
如何体验流上的函数式操作呢?首先,你的头脑中要有一个概念,流是数据管道,是用于操作数据源所生成的元素序列。
OK,到此你已经掌握了六七层,你不信?
别急,且听我慢慢道来……
首先,流式数据管道,你操作一个集合,用 Jio 想你也知道集合不是管道,它是一种存储数据的数据结构,那如何将它变成可以去操作的管道呢?这就涉及到 流的创建 了 ☟
创建 Stream 流常用的几种方式:
Stream 类中创建
Stream.of()
语法:
示例代码:
// 将 value 放入数据处理的管道当中,value 类型为 T
Stream.of("java", "js", 1, 3.0f, 12L).forEach(System.out::println);
Stream.generate()
语法:
示例代码:
// 生成 5 个 0-1 之间的随机数,它们被放到流中并被逐一打印
Stream.generate(Math::random).limit(5).forEach(System.out::println);
Stream.empty()
语法:
示例代码:
// 创建一个空流
Stream<Object> empty = Stream.empty();
Stream.builder()
语法:
示例代码:
// 使用 Stream 中构造者模式,往流中添加元素
Stream.builder()
.add("java")
.add("javascript")
.add("html")
.add("css").build()
.forEach(System.out::println);
Arrays 数组工具类中创建
Arrays.stream()
语法:
示例代码:
// 创建一个整型数组,然后将其变为流进行操作
Arrays.stream(new int[] {1, 3, 5, 7}).forEach(System.out::println);
Collection 集合接口中默认方法创建
Collection.stream()顺序流
语法:
示例代码:
// adults 是一个存储对象的集合,将其变为顺序流(串行流)然后打印输出
adults.stream().forEach(System.out::println);
Collection.parallelStream()并行流
语法:
示例代码:
// adults 是一个存储对象的集合,将其变为并行流然后打印输出
adults.parallelStream().forEach(System.out::println);
区别:
stream() 是转化为顺序流,它使用的是主线程,是单线程的;但是 parallelStream() 是转化为并行流,它是多个线程同时运行的。因此,前一个是按顺序输出的,而后一个则显得无序。
操作流的常用 API
到此,我们知道了如何去创建一个流,创建好的流我们需要操作它,那这就涉及到对流的 中间操作 了 ☟
为了不显俗套,这里以问题的方式一一呈现,这样的目的在于更有使用场景的代入!
首先,给出操作数据源:
private static List<User> users = new ArrayList<>();
private static List<Integer> nums = Arrays.asList(12,23,52,25,21,67,12);
static {
users.add(new User(19, "张三", "男", 19, 3000));
users.add(new User(20, "李四", "女", 15, 2350));
users.add(new User(42, "王麻子", "男", 29, 4000));
users.add(new User(23, "李亮", "女", 39, 2360));
users.add(new User(51, "张亮", "男", 24, 5220));
users.add(new User(27, "李梁", "男", 22, 3333));
users.add(new User(25, "王甜", "女", 11, 2135));
users.add(new User(42, "石天苟", "女", 16, 4520));
}
- 求
nums集合中平均数,如果平均数存在则输出。
OptionalDouble average = nums.stream()
.mapToInt(Integer::intValue)
.average();
// OptionalDouble average = nums.stream().mapToInt(i -> i).average(); 同上
average.ifPresent(System.out::println);
解析:首先,将
nums变成可以操作的stream流,然后使用中间操作方法mapToInt()将流中的每个元素映射成Integer类型,最后使用average()方法求这些整型元素的平均数,返回的结果为OptionalDouble对象,这个对象可以用来判空,只需调用它的ifPresent()方法,字面意思,如果存在值则执行括号中的语句。
- 对
nums集合去重后倒序排列,返回这个新的集合。
List<Integer> newCollect = nums.stream()
.distinct()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(newCollect);
解析:将 nums 集合变成一个流之后,使用
distinct()方法去除重复的元素,然后使用sorted()方法排序,不带参数时,默认升序,如若需要降序,传入比较器中的一个静态方法reverseOrder(),到这一步流并没有转化为集合,需要使用collect(Collectors.toList())将其最终转换为一个新的集合。
- 查询薪资最高的员工姓名及其薪资多少。
Optional<User> highestSalary = users.stream()
.max(Comparator.comparingInt(User::getSalary));
highestSalary.ifPresent( user ->
System.out.println("工资最高的用户:" + user.getName() + " ,
工资为:" + user.getSalary()));
解析:将
User对象集合变成一个可以操作的流管道,通过Comparator.comparingInt(User::getSalary)规定比较的属性以及属性的类型,然后把它放到max()方法中作为参数去找到最大薪资的员工,此时,返回的结果为Optional<User>对象,这个对象可以用来判空,只需调用它的ifPresent()方法,字面意思,如果存在值则执行括号中的语句。
- 将存储
User集合中的Salary全部取出来组装成一个薪资集合,然后求薪资的总额。
List<Integer> salarys = users.stream()
.map(User::getSalary)
.collect(Collectors.toList());
Integer sum = salarys.stream()
.reduce(0, (result, currentSalary) -> result + currentSalary);
System.out.println("薪资总额:" + sum);
解析:首先,将
User对象集合变成一个可以操作的流管道,使用map()方法映射员工的薪资,然后将流转化成List集合,然后再对这个薪资集合做同样的操作,但不是使用映射方法而是使用reduce()归约,也叫做缩减,该方法指定一个容器,专门用来接收从右往左缩减操作后的结果,第一个参数规定了容器的初始值,第二个参数使用了函数式接口,规定了容器变量名以及当前元素,函数的实现则是缩减操作,规定要做什么。这里只是简单的累加,所有把当前值累加到容器中即可,返回的结果就是薪资总额了。
- 找出所有用户名,在用户名前缀为
user_,用、对每一个用户进行分割,拼接后的结果最前面和最后面随意点缀任意字符。
String userNames = users.stream()
.map(user -> "user_"+user.getName())
.collect(Collectors.joining("、", "拼接后的结果:", "-> 小尾巴"));
// Run => 拼接后的结果:user_张三、user_李四、user_王麻子、user_李亮、user_张亮、user_李梁、user_王甜、user_石天苟-> 小尾巴
解析:不赘述了,这里使用
map()映射到每个用户的用户名,然后用Collectors类中静态方法joining(),给用户名添加delimiter分隔符,最后结果添加上prefix前缀,suffix后缀。
- 将工资大于 3000 的用户进行分组
Map<Boolean, List<User>> groupResult = users.stream()
.collect(Collectors.partitioningBy(user -> user.getSalary() > 3000));
// 工资小于等于 3000 的用户集合
List<User> group1 = groupResult.get(false);
// 工资大于 3000 的用户集合
List<User> group2 = groupResult.get(true);
System.out.println("工资小于等于 3000 的用户集合:" + group1);
System.out.println("工资大于 3000 的用户集合:" + group2);
解析:这里主要就是使用了
Collectors类中静态方法partitioningBy(),这个方法的参数是分组的依据,如果满足分组的依据,那么返回键就是true,否则为false,值的话就是分组成员了。
- 将所有员工按照男女进行分组。
Map<String, List<User>> groupResult = users.stream()
.collect(Collectors.groupingBy(User::getSex));
// 男子组
List<User> males = groupResult.get("男");
// 女子组
List<User> female = groupResult.get("女");
System.out.println("男子组:" + males);
System.out.println("女子组:" + female);
解析:是不是有一个问题:为什么上面可以使用
Collectors.partitioningBy()分组,而这里却不用呢?因为partitioningBy()方法参数只认Predicate断言,简单点说,就是真或假的表达式!仍要坚持使用这个方式也不是不可以,就是比较麻烦,需要写一个equal()判断;但是使用Collectors.groupingBy()却可以不用,一般情况下,性别只有男或女,所以只需根据属性值判断是否属于同一组中的。
- 返回一个
Map键值对,键为员工的姓名,值为员工信息。
Map<String, User> map = users.stream().collect(Collectors.toMap(User::getName, user -> user));
System.out.println(map);
解析:这里将流中的操作的对象映射到了
Map集合上,员工名对应Map的key,员工详细信息对应的是Map的value,使用Collectors.toMap()实现。
- 筛选出年龄在 10-25 岁之间且薪资在 1500 以上的员工。
// 通过 Predicate 断言 test
Predicate<User> userPredicate1 = user -> user.getAge() < 25;
Predicate<User> userPredicate2 = user -> user.getAge() > 10;
Predicate<User> userPredicate3 = user -> user.getSalary() > 1500;
List<User> userList =
list1.stream()
.filter(userPredicate1)
.filter(userPredicate2)
.filter(userPredicate3)
.collect(Collectors.toList());
// 多个 filter 可以变为一个
/*
List<User> collect1 =
list1.stream()
.filter(userPredicate1.and(userPredicate2).and(userPredicate3))
.collect(Collectors.toList());
*/
解析:这个案例中使用
filter()方法对管道中的数据进行筛选,方法的参数为Predicate断言,满足断言的留下,不符合条件的剔除,最后返回的结果经过转换成集合。
末端结果返回
末端返回总是在中间操作过后,为了终止流向后传递,返回最终的结果形态,可以是List、Optional、Map、String、Integer 等等,在上述问题案例中均有体现,这里就不再过多赘述了……
总结
为了加深大家记忆,以图高度概括为:
了解更多
如果想了解更多相关用法,且英语水平不错的可以参考 官方文档
结尾
撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。