JAVA8 | Stream流

集合是 Java 中使用最多的 API,几乎每个 Java 程序都需要 制造处理 集合。为了更好地操作集合,JDK 提供了不少工具类,还有不少第三方类库,比如 GuavaCommon Collections 等,但是,还不够。

比如:

在所有职员中,查找年龄小于 40,按工资进行倒序排序,列举工资最高的前十位的职员姓名

JAVA8 以前,需要进行专门处理,如果没有注释辅助,很难一眼看明白这段代码的含义

public static void opBefore(List<Emp> emps) {
    List<Emp> filterEmps = new ArrayList<>();
    for (Emp emp : emps) {
        if (emp.getAge() < 40) {
            filterEmps.add(emp);
        }
    }

    Collections.sort(filterEmps, new Comparator<Emp>() {
        @Override
        public int compare(Emp e1, Emp e2) {
            return Double.compare(e2.getSalary(), e1.getSalary());
        }
    });

    List<String> names = new ArrayList<>();
    for (Emp emp : filterEmps) {
        names.add(emp.getName());
        if (names.size() >= 10) {
            break;
        }
    }
    System.out.println(names);
}

JAVA8 以后呢?JAVA8 的设计者们设计了 ,使用流,只需要如下代码即可表达同样的效果,代码简洁清晰,关键还能一眼看明白含义,看完是不是再也不想写以前的代码了。

public static void opAfter(List<Emp> emps) {
    emps.stream().filter(v -> v.getAge() < 40)
            .sorted(Comparator.comparing(Emp::getSalary).reversed())
            .limit(10)
            .map(Emp::getName)
            .forEach(System.out::println);
}

如果这个列表非常大,需要使用多线程,只需要将 stream() 修改为 parallelStream() 即可。

流是 JDK8 的新成员,具备以下特点:

  • 声明式编程:代码简洁、易读
  • 可复合:在上例中,filter 过滤之后马上对过滤的结果执行 sorted 排序操作,然后对排序结果进行 limit 截断操作,这种管道式编程可以互相组合,从而形成复杂逻辑,非常灵活
  • 可并性:只需要简单地方法调用,即可透明地实现并发编程,不用担心内部实现细节,实现高性能

只遍历一次

流跟迭代器一样,只能遍历一次,再次遍历则会抛出异常,如果想再次遍历,只能通过数据源重新获取一个新的流。

List<String> langs = Arrays.asList("Java", "PHP", "Python"); 
Stream<String> stream = langs.stream();
stream.forEach(System.out::println);        // 打印
stream.forEach(System.out::println);        // 抛出 java.lang.IllegalStateException 异常

外部迭代与内部迭代

使用 Collection 接口进行迭代(比如 for-each),称为外部迭代。Streams 库使用内部迭代。

  • 使用 for-each 进行外部迭代
List<String> names = new ArrayList<>();
for (Emp emp : emps) {
    filterEmps.add(emp.getName());
}
  • 使用 Iterator 进行外部迭代
List<String> names = new ArrayList<>();
Iterator<Emp> iterator = emps.iterator();
while(iterator.hasNext()) {
    Emp emp = iterator.next();
    filterEmps.add(emp.getName());  
}
  • 使用 Stream 内部迭代
List<String> names = emps.stream()
        .map(Emp::getName)
        .collect(toList());

外部迭代与内部迭代的区别。外部迭代需要用户显式地取出每个项目再加以处理,一切都靠用户。内部迭代则由 JDK 进行迭代,JDK 可以透明地进行优化处理,比如根据硬件进行优化,使用并行处理等,随着 JDK 的发展,可以继承 JDK 对内部迭代的优化。

流操作

emps.stream()
        .filter(v -> v.getAge() < 40)
        .sorted(Comparator.comparing(Emp::getSalary).reversed())
        .map(Emp::getName)
        .limit(10)
        .forEach(System.out::println);

中间操作

诸如 filtersortedmap 等,这类操作目标是一个流,操作完成会返回另一个流,这让多个操作可以连接起来,形成一个流水线。中间操作并不会立即处理,而是等待流水线上出现终端操作,在终端操作时一次性全部处理。

方法说明函数描述符
filter过滤T -> boolean
map对象转换T -> R
peek对象处理T -> {}
sorted排序(T, T) -> Integer
limit截断
distinct去重
skip跳过 n 个元素

终端操作

终端操作会从流的流水线生成结果,这个结果可是以统计,归约,也可以是处理函数,比如:上例中的 forEach 接的就是一个处理函数。

方法说明
forEach处理函数,对流中的每个元素进行处理,比如打印至控制台
count返回流中元素的个数
collect归约函数,将流归约成集合,List、Set、Map 等

流的使用

构建流

集合构建流

Collection 的实现类(比如:ListSet)直接调用 stream() 方法即可获取流。

List<String> langs = Arrays.asList("JAVA", "PHP", "PYTHON")
Stream<String> stream = langs.stream();

数组构建流

数组通过调用 Stream.of() 方法获取流。

String[] langs = {"JAVA", "PHP", "Python"};
Stream<String> stream = Stream.of(langs)

构建无限流

通过 Stream.iterate() 方法构建无限流,这个方法参数是一个表达式,通常会结合 limit 方法进行截断,不然真的无限流就只能等 OOM 了。

构建一个偶数的流

  • 第一个参数,定义流的第一个元素
  • 第二个参数,根据流中的最后一个元素,得到一个新的元素,并加入到流中
Stream.iterate(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println);

构建随机无限流

通过 Stream.generate() 方法构建无限流,使用 iterate 构建无限流时,流中的每个元素有延续性,因为向流中加入新的元素是根据上一个元素进行生成的。而 generate 则是无延续性的,适合生成随机流。

Stream.generate(Math::random)
        .limit(5)
        .forEach(System.out::println);

查找与匹配

filter

示例:查找流中的偶数并打印到控制台

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
numbers.stream()
        .filter(v -> v % 2 == 0)
        .forEach(System.out::println);

输出如下

2
4
6
8

findAny & findFirst

通过 findAny 方法查找流中的任意一个元素,这个方法返回的是 Optional 对象

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
numbers.stream()
        .filter(v -> v % 2 == 0)
        .findAny()
        .ifPresent(System.out::println);

输出如下

2

通过 findFirst 方法查找流中的第一个元素,这两个方法的区别在于 findFirst 查找第一个,当流使用并行处理时,需要等待所有结果返回、归约、排序后才能确定是否第一个,而 findAny 则是取任意一个,效率更高。所以如果对顺序没有要求,请使用 findAny

anyMatch & allMatch & noneMatch

anyMatch 用于判断流中是否有符合条件的元素

示例:判断流中是否存在偶数

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().anyMatch(v -> v % 2 == 0));

输出如下

true

allMatchanyMatch 类似,anyMatch 判断流中任意一个匹配成功即可,而 allMatch 则需要流中的所有元素全部匹配。

示例:判断流中是否全部都是偶数

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().allMatch(v -> v % 2 == 0));

输出如下

false

noneMatchallMatch 正好相反,判断流中的所有元素全部不匹配。

示例:判断流中是否全部都不是偶数

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().noneMatch(v -> v % 2 == 0));

输出如下

false

类 SQL 操作

去重

distinct 这个名字看上去是不是有点熟悉,select distinct col ... from table,不错,就是 SQL 语句中的去重关键字,在流计算中变成了一个方法,作用也是去重。

示例

List<Integer> numbers = Arrays.asList(1,2,2,3,3,4);
numbers.stream()
        .distinct()
        .forEach(System.out::println);

输出如下

1
2
3
4

截断与跳过

skip 表示跳过 n 个元素,limit 表示截取多少个元素,这两个方法可以组合成内存分页方法

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
numbers.stream().skip(6).limit(3).forEach(System.out::println);

输出如下

7
8
9

内存分页工具方法

public static <T> List<T> pagination(List<T> rows, int page, int pageSize) {
    if (CollectionUtils.isEmpty(rows)) {
        return Collections.emptyList();
    }
    int totalRows = rows.size();
    int skipRows = (page - 1) * pageSize;
    if (totalRows <= skipRows) {
        return Collections.emptyList();
    }
    return rows.stream().skip(skipRows).limit(pageSize).collect(Collectors.toList());
}

映射

创建一个员工类,供后续示例使用

public class Emp {
    private Long id;
    private String name;
    private Integer age;
    private Double salary;
    private Integer sex;
}

map

map 方法接受一个函数作为参数,对流中每一个元素应用该函数,并返回一个新的元素,更换流中的对象。

示例:打印员工的名称

emps.stream()               // 流中的元素为 Emp 对象
        .map(Emp::getName)  // 流中的元素转换为 String 对象
        .forEach(System.out::println);

示例:为所有员工加薪1000

emps.stream()               // 流中的元素为 Emp 对象
        .map(v -> {
            v.setSalary(v.getSalary() + 1000D);
            return v;
        })                  // 流中的元素还是 Emp 对象
        .collect(Collectors.toList());

peek

如上例所示,在 map 方法中只是对流中的元素进行处理,并不需要更换对象,peek 方法用于处理这种情况

示例:为所有员工加薪1000

emps.stream()
        .peek(v -> v.setSalary(v.getSalary() + 1000D))   // 对流中的元素进行处理,但不更换
        .collect(Collectors.toList());

flatMap

流的扁平化,在前面的示例中,流中的元素都是对象,可以通过 map 方法对每一个元素进行处理,但如果流中的元素是一个集合,该如何操作?

例如:指定一个单词列表 ["Hello", "World"],返回包含的所有字母并去重

首先需要将单词分解成字母,然后对所有字母的集合进行去重操作即可,通过前文中的示例,可以知道 distinct 方法可以去重,于是组合成如下代码

List<String> words = Arrays.asList("Hello", "World");
List<String[]> letters = words.stream()
        .map(v -> v.split(""))
        .distinct()
        .collect(Collectors.toList());

可以看到,返回的结果是 List<String[]> 类型,并不是想象中的字母集合,这是因为 map 方法返回的是一个字符串数组,map 处理之后,流中的元素变成了字母数组 Stream<String[]>,而我们实际想要的是 Stream<String>,通过 flatMap 让流扁平化。

List<String> words = Arrays.asList("Hello", "World");
List<String> letters = words.stream()
        .map(v -> v.split(""))          // 流中元素转换为 String[]
        .flatMap(v -> Stream.of(v))     // 流中元素转换为 String
        .distinct()
        .collect(Collectors.toList());

自己动手

1、给定一个数字列表,[1,2,3,4],返回每个数字的平方构成的列表,结果为[1,4,9,16]
2、给定两个数字列表,[1,2,3] 及 [3,4],对两个列表进行迪卡尔集,结果为[(1,3),(1,4),(2,3),(2,4),(3,3),(3,4)]

解答1

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<Integer> squares = numbers.stream().map(n -> n * n).collect(toList());

解答2

List<Integer> numbers1 = Arrays.asList(1, 2, 3); 
List<Integer> numbers2 = Arrays.asList(3, 4); 
List<int[]> pairs = numbers1.stream()
        .flatMap(i -> numbers2.stream().map(j -> new int[]{i, j}))
        .collect(toList());

归约

在前文中,我们使用了 map 对流中的每个元素进行处理,那么处理完之后,则需要对处理的结果进行收集。在前面的示例中,我们有通过 anyMatch 之类的方法收集为 Boolean 值,或者通过 findAny 之类的方法收集为 Optional 对象,但这些都是定向的收集,流还提供了 reduce 函数用于更复杂的结果归约。

求和

设置初始值

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

无初始值(可能存在流中一个元素都没有的情况)

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

求积

int result = numbers.stream().reduce(1, (a, b) -> a * b);

最大值最小值

求最大值

Optional<Integer> max = numbers.stream().reduce(Integer::max);

求最小值

Optional<Integer> min = numbers.stream().reduce(Integer::min);

统计元素个数

通过 map 将集合中的元素转换为 1,然后通过 reduce 进行累加即可计算出流中的元素个数。

int count = numbers.stream().map(v -> 1).reduce(0, (a, b) -> a + b);

收集器

流提供了 collect 方法进行结果收集,并提供了 Collectors 类,该类提供了很多收集器,可以基本满足日常需求,如果实现满足不了,也可以通过实现自定义收集器进行收集。

计数器

示例:统计流中员工的数量

long count = emps.Stream().collect(Collectors.counting());

还可以简写为

long count = emps.Stream().count()
比较器

示例:查找年龄最大的员工

Optional<Emp> collect = emps.stream().collect(Collectors.maxBy(Comparator.comparing(Emp::getAge)));

也可以简写为

Optional<Emp> collect = emps.stream().max(Comparator.comparing(Emp::getAge));

示例:查找工资最低的员工

Optional<Emp> collect = emps.stream().collect(Collectors.minBy(Comparator.comparing(Emp::getSalary)));

也可以简写为

Optional<Emp> collect = emps.stream().min(Comparator.comparing(Emp::getSalary));
汇总收集

Collectors 提供了 summingIntsummingLongsummingDouble 三个收集器,用于对相应的数据类型进行求和计算。

示例:计算所有员工的薪资总和

Double sumSalary = emps.stream().collect(Comparator.summingDouble(Emp::getSalary));

averagingIntaveragingLongaveragingDouble 三个收集器,用于对相应的数据类型进行求平均值。

示例:计算员工的平均薪资

Double avgSalary = emps.stream().collect(Comparator.averagingDouble(Emp::getSalary));

summarizingIntsummarizingLongsummarizingDouble 三个收集器,用于对相应的数据类型进行统计,统计结果包括了记录数、汇总值、平均值、最大值、最小值。

DoubleSummaryStatistics collect = emps.stream().collect(Collectors.summarizingDouble(Emp::getSalary));
System.out.println("员工总数:" + collect.getCount());
System.out.println("员工总薪资:" + collect.getSum());
System.out.println("员工平均薪资:" + collect.getAverage());
System.out.println("员工最高薪资:" + collect.getMax());
System.out.println("员工最低薪资:" + collect.getMin());
字符串连接收集器

相当于 String.join() 方法,将字符串集合连接成字符串,可以指定元素之间的间隔字符。

示例:获取所有员工的姓名,并以字符串形式输出

String names = emps.stream().map(Emp::getName).collect(Collectors.joining(","));
List 收集器

示例:获取员工的姓名列表

List<String> names = emps.Stream()
        .map(Emp::getName)
        .collect(Collectors.toList());
Set 收集器

通过 Set 本身的特性可以实现去重

示例:获取员工的 ID 集合

Set<String> names = emps.Stream()
        .map(Emp::getId)
        .collect(Collectors.toSet());
Map 收集器

示例:获得 员工ID -> 员工信息 的键值对

Map<Long, Emp> names = emps.Stream()
        .map(Emp::getName)
        .collect(Collectors.toMap(Emp::getId, Function.identity()))

如果出现 id 一样的记录,以上代码就会出现异常,因为产生了键冲突,又没有提供一个解决冲突的方案,事实上 toMap 方法的第三个参数就是解决冲突的函数,如下例,如果出现键冲突,则抛弃后来的元素,保留第一个。

Map<Long, Emp> names = emps.Stream()
        .map(Emp::getName)
        .collect(Collectors.toMap(Emp::getId, Function.identity(), (v1, v2) -> v1))
分组收集器

示例:根据性别进行分组

Map<Integer, List<Emp>> sexGroup = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex));

示例:根据年龄进行分组

Map<String, List<Emp>> ageGroup = emps.stream()
        .collect(Collectors.groupingBy(v -> {
            return v.getAge() <= 35 ? "safety" : "dangerous";
        }));
多级分组收集器

groupingBy 收集器还可以接收第二个参数,类型是一个收集器,可以理解为在组内进行二次收集,那么再传入一个分组收集器,则可以实现多级分组收集

示例:先按性别,然后按年龄进行二级分组

Map<Integer, Map<String, List<Emp>>> collect = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, Collectors.groupingBy(v -> {
            return v.getAge() <= 35 ? "safety" : "dangerous";
        })));
组内收集

groupingBy 的第二个参数为收集器,可以进行组内收集。

结合 counting 收集器,统计组内记录数

示例:查找不同性别的人数总和

Map<Integer, Long> sexCount = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, Collectors.counting()));

结合 maxBy 收集器,查找组内最大值

示例:查找不同性别年龄最大的员工

Map<Integer, Optional<Emp>> maxAge = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, Collectors.maxBy(Comparator.comparing(Emp::getAge))));
组内收集并转换

Collectors 提供了 collectingAndThen 收集器,collectingAndThen 的第一个参数是一个收集器,也是实际执行的收集器,第二个参数为转换器,用于对收集器的收集结果进行转换。

示例:查找不同性别年龄最大的员工

Map<Integer, Emp> maxAge = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, 
            Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Emp::getAge)), Optional::get)));
组内转换

Collectors 提供了 mapping 收集器,可以对组内元素进行转换并收集

示例:获取不同性别员工的姓名集合

Map<Integer, List<String>> collect = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, 
            Collectors.mapping(Emp::getName, Collectors.toList())));

示例:按性别进行分组,每个组内转换为 id -> name 的键值对

Map<Integer, Map<Long, String>> collect = emps.stream()
        .collect(Collectors.groupingBy(Emp::getSex, 
            Collectors.mapping(Function.identity(), Collectors.toMap(Emp::getId, Emp::getName))));
分区

Collectors 提供了 partitioningBy 分区收集器,这个收集器跟 groupBy 收集器类似,只是收集的 Key 规定为 Boolean 类型

示例:按年龄进行分区,35岁以下的为 true,否则为 false,分为两组

Map<Boolean, List<Emp>> collect = emps.stream()
        .collect(Collectors.partitioningBy(v -> v.getAge() <= 35));
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值