平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用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<>();
串行流线程安全,保证顺序;并行流线程不安全,不保证顺序,但是快。
// 串行流 Stream<User> stream = users.stream(); // 并行流 Stream<User> stream = users.parallelStream();
Stream.of()
方法底层仍然用得是Arrays.stream()。
String[] userNameArray = {"mary", "jack", "tom"}; // 方法1 Stream<String> stream = Arrays.stream(userNameArray); // 方法2 Stream<String> stream = Stream.of(userNameArray);
Stream.of()
方法可接收可变参数,T... values。
Stream<String> stream = Stream.of("mary", "jack", "tom");
处理原始类型int、double、long
IntStream intStream = IntStream.of(1, 2, 3);
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());
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.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);
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);
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);
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();
我们想要换取 所有用户 的 所有账号 ,比如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());
实际工作中,我们可能存在对一个集合多次中间操作后,经过不同的终端操作产生不同的结果这一需求。这个时候,我们就产生想要流能够复用的想法,但是实际上当一个流调用终端操作后,该流就会被关闭,如果关闭后我们再一次调用终端操作,则会产生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));
根据基础类型和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());
根据基础类型和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));
计算平均值:
// 方法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);
关于流的一些常用操作就介绍完啦~希望大家能有所收获。我是宋影,第一篇技术类博文就此奉上啦。
参考博文:
https://juejin.cn/post/6844903830254010381#heading-9
https://blog.csdn.net/sinat_36184075/article/details/111767670
https://colobu.com/2016/03/02/Java-Stream/
http://www.itwanger.com/life/2020/04/01/java-stream.html