飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

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

时间:2022-02-03  作者:yumozhan  


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

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

 

一、流的组成

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

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

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

  3. 终端操作:一条语句中有且只存在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);

 

七、结尾

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

参考博文:

  1. https://域名/post/6844903830254010381#heading-9

  2. https://域名/sinat_36184075/article/details/111767670

  3. https://域名/2016/03/02/Java-Stream/

  4. http://域名/life/2020/04/01/java-域名

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。