Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)
平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用api的场景,这篇文章的目的,主要是为介绍一些工作中使用流时的常用操作,例如去重、排序和数学运算等内容,并不对流的原理和各种高级api做深度剖析,让我们开始吧~
如果读者你已经对流有一些基本的了解,现在只是有些场景运用到流,不知道如何使用,请划到文章的最后一个部分-常用操作,希望能够帮助到你。^^
一、流的组成
往往我们使用流的时候,都会经过3步,如下图所示,首先我们创建一个流,然后对流进行一系列的中间操作,最后执行一个终端操作,这个流就到此结束了。
-
创建流:有且创建一次即可。
-
中间操作:0个,1个及多个均可,可以进行链式操作。
-
终端操作:一条语句中有且只存在1个,一旦进行该操作,代表该流已结束。
我们需要关注的,实际上是对流的中间操作和终端操作。
二、举例对象
例子:现在我们多个用户,抽象成List<User>
,该用户有ID,名称,年龄,钱以及拥有多个账户。
@Data
public class User{
private Integer id;
private String name;
private int age;
private BigDecimal money;
private List<Account> accounts;
}
// 操作
List<User> users = new ArrayList<>();
三、创建流
3.1 Collection集合
串行流线程安全,保证顺序;并行流线程不安全,不保证顺序,但是快。
// 串行流
Stream<User> stream = 域名am();
// 并行流
Stream<User> stream = 域名llelStream();
3.2 数组
域名()
方法底层仍然用得是域名am()。
String[] userNameArray = {"mary", "jack", "tom"};
// 方法1
Stream<String> stream = 域名am(userNameArray);
// 方法2
Stream<String> stream = 域名(userNameArray);
3.3 多个元素
域名()
方法可接收可变参数,T... values。
Stream<String> stream = 域名("mary", "jack", "tom");
3.4 特殊类型流
处理原始类型int、double、long
IntStream intStream = 域名(1, 2, 3);
四、中间操作
4.1 映射和消费
map()
:可将集合中的元素映射成其他元素。例如 List<User> -> List<String>
flatmap()
:将映射后的元素放入新的流中,可将集合中元素的某个集合属性扁平化。例如List<List<Account>> -> List<Account>
peek
:对集合中的元素进行一些操作,不映射。例如List<User> -> List<User>
// map
List<String> userNames = 域名am().map(User::getName).collect(域名st());
// flatmap
List<Account> accounts = 域名am().map(User::getAccounts).flatMap(Collection::stream).collect(域名st());
// peek
List<User> newUsers = 域名am().peek(user -> 域名ame("Jane")).collect(域名st());
4.2 过滤和去重
filter()
:保留符合条件的所有元素。
distinct()
:根据hashCode()和equals方法进行去重。
skip(n)
:跳过前n个元素。
limit(n)
:获取前n个元素
// filter(常用)
List<User> newUsers = 域名am().filter(user -> 域名ge() > 15).collect(域名st());
// distinct
List<User> newUsers = 域名am().distinct().collect(域名st());
// limit
List<User> newUsers = 域名am().skip(2).collect(域名st());
// skip
List<User> newUsers = 域名am().limit(2).collect(域名st());
五、终端操作
5.1 收集
5.1.1 collect()
collect()
:将流中的元素收集成新的对象,例如List, Set, Map
等,这个方法有两种参数,我们常用的是第一种,利用Collectors
工具类来获取Collector
对象,第二种在实际工作中用得少,本文便不介绍,读者有兴趣可去自行了解。:p
-
collect(Collector)
:(常用) -
collect(Supplier, BiConsumer, BiConsumer)
收集
// list
List<User> newUsers = 域名am().collect(域名st());
// set
Set<User> newUsers = 域名am().collect(域名t());
// map
// toMap():
// 第一个参数是map的key;
// 第二个参数是map的value(域名tity()代表取自身的值);
// 第三个参数是key相同时的操作(本行代表key相同时,后面的value覆盖前面的value)
Map<Integer, User> map = 域名am().collect(域名p(User::getId, 域名tity(), (v1, v2) -> v1));
分组
// 根据对象中某个字段分组
Map<Integer, List<User>> map = 域名am().collect(域名pingBy(User::getId));
// 根据对象中某个字段分组后,再根据另外一个字段分组
Map<Integer, Map<String, List<User>>> map = 域名am().collect(域名pingBy(User::getId, 域名pingBy(User::getName)));
拼接
// 拼接,比如"hello", "world" -> "hello,world"
String str = 域名am().map(User::getName).collect(域名ing(","));
5.1.2 toArray()
toArray()
:将List的流收集成数组Array。
// 可利用String[]::new来指定类型
String[] userNames = 域名am().map(User::getName).toArray(String[]::new);
5.2 断言
allMatch()
:所有元素符合条件则返回true,否则返回false。 noneMatch()
:所有元素都不符合条件则返回true,否则返回false。 anyMatch()
:存在元素符合条件则返回true,否则返回false。
// 是否所有的用户年龄都大于15
boolean allMatch = 域名am().allMatch(user -> 域名ge() > 15);
// 是否所有的用户年龄都不大于15
boolean noneMatch = 域名am().noneMatch(user -> 域名ge() > 15);
// 是否存在用户年龄大于15
boolean anyMatch = 域名am().anyMatch(user -> 域名ge() > 15);
5.3 规约
reduce()
:可以将流的元素组合成一个新的结果。
这个API,我在实际工作中用得很少……可能在计算BigDecimal之和的时候才会用到:
BigDecimal sum = 域名am().map(User::getMoney).reduce(域名, BigDecimal::add);
// 指定初始值:
// 相当于new User(1 + users中所有的ID之和,"1", 0, 0)
User user1 = 域名am().reduce(new User(1, "1", 0, 0), (u1, u2) -> {
域名d(域名d() + 域名d());
return u1;
});
// 不指定初始值:
// 相当于new User(users中所有的ID之和,"1", 0, 0)
User user2 = 域名am().reduce((u1, u2) -> {
域名d(域名d() + 域名d());
return u1;
}).orElse(null);
5.4 过滤
findAny()
:返回流中任意一个元素,如果流为空,返回空的Optional。
findFirst()
:返回流中第一个元素,如果流为空,返回空的Optional。
并行流,findAny会更快,但是可能每次返回结果不一样。
// findAny()
Optional<User> optional = 域名am().findAny();
// findFirst
Optional<User> optional = 域名am().findFirst();
// 建议先用isPresent判空,再get。
User user = 域名();
六、常用操作
6.1 扁平化
我们想要换取 所有用户 的 所有账号 ,比如List<Account>
,可以使用flatMap
来实现。
两种方法获取结果一模一样。
// 方法1:
List<Account> accounts = 域名am()
.flatMap(user -> 域名ccounts().stream())
.collect(域名st());
// 方法2:
List<Account> accounts = 域名am()
.map(User::getAccounts)
.flatMap(Collection::stream)
.collect(域名st());
6.2 流的逻辑复用
实际工作中,我们可能存在对一个集合多次中间操作后,经过不同的终端操作产生不同的结果这一需求。这个时候,我们就产生想要流能够复用的想法,但是实际上当一个流调用终端操作后,该流就会被关闭,如果关闭后我们再一次调用终端操作,则会产生stream has already been operated upon or closed
这个Exception,我们无奈之下,只好把相同的逻辑,重复再写一遍……
如果想使得流逻辑复用,我们可以用Supplier接口把流包装起来,这样就可以实现啦。
不过要注意一点,并不是流复用,而是产生流的逻辑复用,其实还是生成了多个流。
比如我们想要15岁以上的:(1)所有用户集合;(2)根据ID分组后的集合。
// 1. 复用的逻辑
Supplier<Stream<User>> supplier = () -> 域名am().filter(user -> 域名ge() > 15);
// 2.1 所有用户集合
List<User> list = 域名().collect(域名st());
// 2.2 根据ID分组后的集合
Map<Integer, List<User>> map = 域名().collect(域名pingBy(User::getId));
6.3 排序
根据基础类型和String类型排序:
比如List<Integer>
和List<String>
集合,可使用sorted()
排序, 默认升序。
注意:例如"123",字符串类型的数字不可直接比较,因为它是根据ASCII码值来比较排序的。
// 升序 {3, 2, 4} -> {2, 3, 4}
List<Integer> newList = 域名am().sorted().collect(域名st());
// 降序 {3, 2, 4} -> {4, 2, 3}
List<Integer> newList = 域名am().sorted(域名rseOrder()).collect(域名st());
根据对象中某个字段排序:
根据ID进行排序。
// 升序
List<User> newUsers = 域名am().sorted(域名aring(User::getId)).collect(域名st());
// 降序
List<User> newUsers = 域名am().sorted(域名aring(User::getId).reversed()).collect(域名st());
// 先根据ID排序,再根据age排序
List<User> newUsers = 域名am().sorted(域名aring(User::getId).thenComparing(User::getAge)).collect(域名st());
其中User可能为null,User中的ID也可能为null。
-
方法1:先过滤,再排序
-
方法2:可使用nullFirst或者nullLast
// 2.1 如果User可能为null
List<User> newUsers = 域名am().sorted(域名sLast(域名aring(User::getId))).collect(域名st());
// 2.2 如果User中的ID可能为null
List<User> newUsers = 域名am().sorted(域名aring(User::getId, 域名sLast(域名ralOrder()))).collect(域名st());
6.4 去重
根据基础类型和String类型去重:
比如List<Integer>
和List<String>
集合,可使用distinct()
去重。
List<Integer> newList = 域名am().distinct().collect(域名st());
根据对象中某个或多个字段去重:
ID有可能相同,根据ID进行去重。
// 方法一:使用TreeSet去重,但是这个方法有副作用,会根据ID排序(TreeSet特性)
List<User> newUsers = 域名am().collect(域名ectingAndThen(
域名llection(() -> new TreeSet<>(域名aring(User::getId))), ArrayList::new));
// 方法二:使用Map的key不可重复的特性,进行去重
List<User> newUsers = 域名am().collect(域名p(User::getId, b -> b, (b1, b2) -> b2))
.values().stream().collect(域名st());
// 方法三:自定义方法去重
List<User> newUsers = 域名am().filter(distinctByKey(User::getId)).collect(域名st());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> 域名fAbsent(域名y(t), 域名) == null;
}
根据ID和Age两个字段进行去重。
List<User> newUsers = 域名am().filter(distinctByKey(User::getId, User::getAge)).collect(域名st());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor1, Function<? super T, ?> keyExtractor2) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> 域名fAbsent(域名y(t).toString() + 域名y(t).toString(), 域名) == null;
}
其中User可能为null,User中的ID也可能为null(参考排序)。
// 如果User中的ID可能为null:可使用nullFirst或者nullLast
List<User> newUsers = 域名am().collect(域名ectingAndThen(
域名llection(() -> new TreeSet<>(域名aring(User::getId,
域名sFirst(域名ralOrder())))), ArrayList::new));
6.5 数学运算
计算平均值:
// 方法1:mapToInt会将当前流转换成IntStream
double average = 域名am().mapToInt(User::getAge).average().getAsDouble()
double average = 域名am().mapToInt(User::getAge).summaryStatistics().getAverage();
// 方法2:Collectors实现的平均数
double average = 域名am().collect(域名agingInt(User::getAge));
计算总和:
// BigDecimal
BigDecimal sum = 域名am().map(User::getMoney).reduce(域名, BigDecimal::add);
// int、double、long:
int sum = 域名oInt(User::getNum).sum;
计算最大值:
找到年龄最大的用户。
int age = 域名am().max(域名aring(User::getAge)).orElse(null);
计算最小值:
找到年龄最小的用户。
int age = 域名am().min(域名aring(User::getAge)).orElse(null);
七、结尾
关于流的一些常用操作就介绍完啦~希望大家能有所收获。我是宋影,第一篇技术类博文就此奉上啦。
参考博文:
https://域名/post/6844903830254010381#heading-9
https://域名/sinat_36184075/article/details/111767670
https://域名/2016/03/02/Java-Stream/
http://域名/life/2020/04/01/java-域名