Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)

时间:2022-12-02 17:48:25

平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用api的场景,这篇文章的目的,主要是为介绍一些工作中使用流时的常用操作,例如去重、排序和数学运算等内容,并不对流的原理和各种高级api做深度剖析,让我们开始吧~

如果读者你已经对流有一些基本的了解,现在只是有些场景运用到流,不知道如何使用,请划到文章的最后一个部分-常用操作,希望能够帮助到你。^^

一、流的组成

往往我们使用流的时候,都会经过3步,如下图所示,首先我们创建一个流,然后对流进行一系列的中间操作,最后执行一个终端操作,这个流就到此结束了。

  1. 创建流:有且创建一次即可。

  2. 中间操作:0个,1个及多个均可,可以进行链式操作。

  3. 终端操作:一条语句中有且只存在1个,一旦进行该操作,代表该流已结束。

我们需要关注的,实际上是对流的中间操作和终端操作。

Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)

二、举例对象

例子:现在我们多个用户,抽象成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);

七、结尾

关于流的一些常用操作就介绍完啦~希望大家能有所收获。我是宋影,第一篇技术类博文就此奉上啦。

参考博文:

  1. https://juejin.cn/post/6844903830254010381#heading-9

  2. https://blog.csdn.net/sinat_36184075/article/details/111767670

  3. https://colobu.com/2016/03/02/Java-Stream/

  4. http://www.itwanger.com/life/2020/04/01/java-stream.html