平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用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 = users.stream();
// 并行流
Stream<User> stream = users.parallelStream();
3.2 数组
Stream.of()
方法底层仍然用得是Arrays.stream()。
String[] userNameArray = {"mary", "jack", "tom"};
// 方法1
Stream<String> stream = Arrays.stream(userNameArray);
// 方法2
Stream<String> stream = Stream.of(userNameArray);
3.3 多个元素
Stream.of()
方法可接收可变参数,T... values。
Stream<String> stream = Stream.of("mary", "jack", "tom");
3.4 特殊类型流
处理原始类型int、double、long
IntStream intStream = IntStream.of(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 = users.stream().map(User::getName).collect(Collectors.toList());
// flatmap
List<Account> accounts = users.stream().map(User::getAccounts).flatMap(Collection::stream).collect(Collectors.toList());
// peek
List<User> newUsers = users.stream().peek(user -> user.setName("Jane")).collect(Collectors.toList());
4.2 过滤和去重
filter()
:保留符合条件的所有元素。
distinct()
:根据hashCode()和equals方法进行去重。
skip(n)
:跳过前n个元素。
limit(n)
:获取前n个元素
// filter(常用)
List<User> newUsers = users.stream().filter(user -> user.getAge() > 15).collect(Collectors.toList());
// distinct
List<User> newUsers = users.stream().distinct().collect(Collectors.toList());
// limit
List<User> newUsers = users.stream().skip(2).collect(Collectors.toList());
// skip
List<User> newUsers = users.stream().limit(2).collect(Collectors.toList());
五、终端操作
5.1 收集
5.1.1 collect()
collect()
:将流中的元素收集成新的对象,例如List, Set, Map
等,这个方法有两种参数,我们常用的是第一种,利用Collectors
工具类来获取Collector
对象,第二种在实际工作中用得少,本文便不介绍,读者有兴趣可去自行了解。:p
-
collect(Collector)
:(常用) -
collect(Supplier, BiConsumer, BiConsumer)
收集
// list
List<User> newUsers = users.stream().collect(Collectors.toList());
// set
Set<User> newUsers = users.stream().collect(Collectors.toSet());
// map
// toMap():
// 第一个参数是map的key;
// 第二个参数是map的value(Function.identity()代表取自身的值);
// 第三个参数是key相同时的操作(本行代表key相同时,后面的value覆盖前面的value)
Map<Integer, User> map = users.stream().collect(Collectors.toMap(User::getId, Function.identity(), (v1, v2) -> v1));
分组
// 根据对象中某个字段分组
Map<Integer, List<User>> map = users.stream().collect(Collectors.groupingBy(User::getId));
// 根据对象中某个字段分组后,再根据另外一个字段分组
Map<Integer, Map<String, List<User>>> map = users.stream().collect(Collectors.groupingBy(User::getId, Collectors.groupingBy(User::getName)));
拼接
// 拼接,比如"hello", "world" -> "hello,world"
String str = users.stream().map(User::getName).collect(Collectors.joining(","));
5.1.2 toArray()
toArray()
:将List的流收集成数组Array。
// 可利用String[]::new来指定类型
String[] userNames = users.stream().map(User::getName).toArray(String[]::new);
5.2 断言
allMatch()
:所有元素符合条件则返回true,否则返回false。 noneMatch()
:所有元素都不符合条件则返回true,否则返回false。 anyMatch()
:存在元素符合条件则返回true,否则返回false。
// 是否所有的用户年龄都大于15
boolean allMatch = users.stream().allMatch(user -> user.getAge() > 15);
// 是否所有的用户年龄都不大于15
boolean noneMatch = users.stream().noneMatch(user -> user.getAge() > 15);
// 是否存在用户年龄大于15
boolean anyMatch = users.stream().anyMatch(user -> user.getAge() > 15);
5.3 规约
reduce()
:可以将流的元素组合成一个新的结果。
这个API,我在实际工作中用得很少……可能在计算BigDecimal之和的时候才会用到:
BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);
// 指定初始值:
// 相当于new User(1 + users中所有的ID之和,"1", 0, 0)
User user1 = users.stream().reduce(new User(1, "1", 0, 0), (u1, u2) -> {
u1.setId(u1.getId() + u2.getId());
return u1;
});
// 不指定初始值:
// 相当于new User(users中所有的ID之和,"1", 0, 0)
User user2 = users.stream().reduce((u1, u2) -> {
u1.setId(u1.getId() + u2.getId());
return u1;
}).orElse(null);
5.4 过滤
findAny()
:返回流中任意一个元素,如果流为空,返回空的Optional。
findFirst()
:返回流中第一个元素,如果流为空,返回空的Optional。
并行流,findAny会更快,但是可能每次返回结果不一样。
// findAny()
Optional<User> optional = users.stream().findAny();
// findFirst
Optional<User> optional = users.stream().findFirst();
// 建议先用isPresent判空,再get。
User user = optional.get();
六、常用操作
6.1 扁平化
我们想要换取 所有用户 的 所有账号 ,比如List<Account>
,可以使用flatMap
来实现。
两种方法获取结果一模一样。
// 方法1:
List<Account> accounts = users.stream()
.flatMap(user -> user.getAccounts().stream())
.collect(Collectors.toList());
// 方法2:
List<Account> accounts = users.stream()
.map(User::getAccounts)
.flatMap(Collection::stream)
.collect(Collectors.toList());
6.2 流的逻辑复用
实际工作中,我们可能存在对一个集合多次中间操作后,经过不同的终端操作产生不同的结果这一需求。这个时候,我们就产生想要流能够复用的想法,但是实际上当一个流调用终端操作后,该流就会被关闭,如果关闭后我们再一次调用终端操作,则会产生stream has already been operated upon or closed
这个Exception,我们无奈之下,只好把相同的逻辑,重复再写一遍……
如果想使得流逻辑复用,我们可以用Supplier接口把流包装起来,这样就可以实现啦。
不过要注意一点,并不是流复用,而是产生流的逻辑复用,其实还是生成了多个流。
比如我们想要15岁以上的:(1)所有用户集合;(2)根据ID分组后的集合。
// 1. 复用的逻辑
Supplier<Stream<User>> supplier = () -> users.stream().filter(user -> user.getAge() > 15);
// 2.1 所有用户集合
List<User> list = supplier.get().collect(Collectors.toList());
// 2.2 根据ID分组后的集合
Map<Integer, List<User>> map = supplier.get().collect(Collectors.groupingBy(User::getId));
6.3 排序
根据基础类型和String类型排序:
比如List<Integer>
和List<String>
集合,可使用sorted()
排序, 默认升序。
注意:例如"123",字符串类型的数字不可直接比较,因为它是根据ASCII码值来比较排序的。
// 升序 {3, 2, 4} -> {2, 3, 4}
List<Integer> newList = list.stream().sorted().collect(Collectors.toList());
// 降序 {3, 2, 4} -> {4, 2, 3}
List<Integer> newList = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
根据对象中某个字段排序:
根据ID进行排序。
// 升序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId)).collect(Collectors.toList());
// 降序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).reversed()).collect(Collectors.toList());
// 先根据ID排序,再根据age排序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).thenComparing(User::getAge)).collect(Collectors.toList());
其中User可能为null,User中的ID也可能为null。
-
方法1:先过滤,再排序
-
方法2:可使用nullFirst或者nullLast
// 2.1 如果User可能为null
List<User> newUsers = users.stream().sorted(Comparator.nullsLast(Comparator.comparing(User::getId))).collect(Collectors.toList());
// 2.2 如果User中的ID可能为null
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId, Comparator.nullsLast(Comparator.naturalOrder()))).collect(Collectors.toList());
6.4 去重
根据基础类型和String类型去重:
比如List<Integer>
和List<String>
集合,可使用distinct()
去重。
List<Integer> newList = list.stream().distinct().collect(Collectors.toList());
根据对象中某个或多个字段去重:
ID有可能相同,根据ID进行去重。
// 方法一:使用TreeSet去重,但是这个方法有副作用,会根据ID排序(TreeSet特性)
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId))), ArrayList::new));
// 方法二:使用Map的key不可重复的特性,进行去重
List<User> newUsers = users.stream().collect(Collectors.toMap(User::getId, b -> b, (b1, b2) -> b2))
.values().stream().collect(Collectors.toList());
// 方法三:自定义方法去重
List<User> newUsers = users.stream().filter(distinctByKey(User::getId)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
根据ID和Age两个字段进行去重。
List<User> newUsers = users.stream().filter(distinctByKey(User::getId, User::getAge)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor1, Function<? super T, ?> keyExtractor2) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor1.apply(t).toString() + keyExtractor2.apply(t).toString(), Boolean.TRUE) == null;
}
其中User可能为null,User中的ID也可能为null(参考排序)。
// 如果User中的ID可能为null:可使用nullFirst或者nullLast
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId,
Comparator.nullsFirst(Comparator.naturalOrder())))), ArrayList::new));
6.5 数学运算
计算平均值:
// 方法1:mapToInt会将当前流转换成IntStream
double average = users.stream().mapToInt(User::getAge).average().getAsDouble()
double average = users.stream().mapToInt(User::getAge).summaryStatistics().getAverage();
// 方法2:Collectors实现的平均数
double average = users.stream().collect(Collectors.averagingInt(User::getAge));
计算总和:
// BigDecimal
BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);
// int、double、long:
int sum = users.stream.mapToInt(User::getNum).sum;
计算最大值:
找到年龄最大的用户。
int age = users.stream().max(Comparator.comparing(User::getAge)).orElse(null);
计算最小值:
找到年龄最小的用户。
int age = users.stream().min(Comparator.comparing(User::getAge)).orElse(null);
七、结尾
关于流的一些常用操作就介绍完啦~希望大家能有所收获。我是宋影,第一篇技术类博文就此奉上啦。
参考博文: