当前位置:   article > 正文

5. Java8新特性-使用流(Using Stream)

using stream

本节会详解介绍如何使用流,以发挥出它的强大功能,主要包括:
1)筛选,切片,映射
2)查找,匹配和归约
3)使用数值范围等数值流
4)从多个源创建流
5)无限流

筛选和切片
用谓词筛选

Stream接口支持filter方法,该方法接收Predicate谓词,而Predicate之前已经介绍了是函数式接口,所以我们可以很方便的传递lambda,对流元素做筛选。

筛除重复的元素

类似sql中的distinct关键字,Stream接口也提供了distinct方法,用于将重复元素过滤出去,如:

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

// result: 1,2,3,4,5
  • 1
  • 2
  • 3
  • 4
截断流

通过limit(n)方法,会返回一个不超过给定长度的流。示例:

List<Dish> dishes = menu.stream().filter(e -> e.getCalories() > 300).limit(3).collect(toList());

// 只筛选出符合谓词的前三个元素就立即返回结果,实现了截断效果
  • 1
  • 2
  • 3
跳过元素

用的比较少,如可以跳过指定个数的元素:skip(n)。 如果流中元素不足n个,则返回一个空流。示例:

List<Dish> dishes = menu.stream().filter(e -> e.getCalories() > 300).skip(1).collect(toList());
  • 1
映射(map)

这是一个非常常用的功能,如取对象某个属性再做处理,就像sql中取某一列。

对流中的每一个元素应用函数

Stream支持map()方法,接收一个函数(Function),这个函数会被应用到每个元素上,并将其映射为新的元素。注意调用map()方法后会改变流的类型,变成映射后新元素的类型。如:

List<String> dishes = menu.stream().filter(e -> e.getCalories() > 300).map(Dish::getName).collect(toList());
  • 1
流的扁平化

有时候需要实现这样的功能:
对于一张单 词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表 [“Hello”,“World”],你想要返回列表[“H”,“e”,“l”, “o”,“W”,“r”,“d”]。
只用map无法解决,你可能想这样实现:

words.stream().map(w -> w.split("")).distinct().collect(toList());
  • 1

但实际达不到需要的效果,因为调用map后生成的流类型是Stream<String[]>, 而我们期望的是Stream<String>。这时需要用到flatMap来解决。

words.stream()
     .map(w -> w.split(""))
     .flatMap(Arrays::stream) // 流的扁平化:将多个流合并为一个流
     .distinct()
     .collect(Collectors.toList());
  • 1
  • 2
  • 3
  • 4
  • 5
查找和匹配

一个常见的数据处理操作是看集合中是否有满足某个条件的数据。Stream API通过allMatch, anyMatch, noneMatch, findFirst, finaAny方法提供了类似功能。

检查至少匹配一个(anyMatch)
public class MatchSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        boolean match = menus.stream().anyMatch(e -> e.getCalories() > 300);

        if (match) {
            System.out.println("存在卡路里大于300的食物");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
所有流元素都匹配(AllMatch)
 public class AllMatchSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        boolean match = menus.stream().allMatch(e -> e.getCalories() > 300);

        if (match) {
            System.out.println("所有食物卡路里大于300");
        } else {
            System.out.println("不是所有食物卡路里大于300");

        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
所有流元素都不匹配(NoneMatch)
public class NoneMatchSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        boolean match = menus.stream().noneMatch(e -> e.getCalories() > 300);

        if (match) {
            System.out.println("所有食物卡路里都不大于300");
        } else {
            System.out.println("有食物卡路里大于300");

        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
查找元素
Optional介绍

java.util.Optional<T>是一个容器类,java8新加入,可代表一个值存在or不存在,引入的目的是为了避免直接返回null。
Optional中几个常见的方法:
1)isPresent(),将在包含值时返回true,反之返回false
2) ifPresent(Consumer<T> c) 会在值存在时执行指定的消费逻辑
3)T get() 会在值存在时返回值,否则返回NoSuchElement异常
4)T orElse(T other)会在值存在时返回值本身,否则返回一个默认值other。

如需要实现如下功能:

// 打印任意一个卡路里大于400的菜肴
menu.stream()
    .filter(e -> e.getCalaris() > 400)
    .findAny()
    .ifPresent(e -> System.out.println(e.getName()));
  • 1
  • 2
  • 3
  • 4
  • 5
查找第一个元素&任意元素

findFirst: 查走第一个元素
findAny: 查找任意一个元素

// 打印任意一个卡路里大于400的菜肴
menu.stream()
    .filter(e -> e.getCalaris() > 400)
    .findAny()
    .ifPresent(e -> System.out.println(e.getName()));

// 打印第一个卡路里大于400的菜肴
menu.stream()
    .filter(e -> e.getCalaris() > 400)
    .findFirst()
    .ifPresent(e -> System.out.println(e.getName()));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

findFirst vs findAny
当明确需要取首元素时使用findFirst,否则用findAny即可。因为它在使用并行流时限制较少。

规约(Reduce)

目前用过的流终端操作有:
1)返回boolean型:anyMatch, allMatch, NoneMatch
2) 返回void: forEach
3)返回Integer: count
4) 返回Optional: findFirst, findAny
5)返回集合:collect(toList)

下面介绍一种新的终端操作:reduce。即可以使用归约操作将流中的元素组合起来,实现强大的功能。如计算菜肴的总卡路里,或挑选出卡路里最高的菜。

元素求和

示例如下:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  • 1
  • 2

reduce接收两个参数:
1)一个初始值,这里是0
2)一个BinaryOperator<T>将两个元素结合起来产生一个新值。

整体过程如下:
1)首先0作为第一个参数a的值,从流中取出值作为b的值,执行求和得到累计值。
2)将累计值作为a的值,再去流中下一个元素作为b的值,再执行求和得到累计值。
3)依次迭代直到流中最后一个元素,终止,得到最后的归约结果。

元素求最大,最小
// 求最大值
public class MaxReduceSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));
        
        //Integer maxCalories = menus.stream().map(Dish::getCalories).reduce(0, (i, j) -> i > j ? i : j);
        Integer maxCalories = menus.stream().map(Dish::getCalories).reduce(0, Integer::max);
        System.out.println(maxCalories);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

求最小值不再举例,写法几乎一致

map-reduce通常一起用,称为map-reduce模式, 因Google用它来进行网络搜索而出名,因为它很容易并行化

目前已了解的中间操作和终端操作汇总

在这里插入图片描述

数值流

还是先看下求和的例子:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  • 1
  • 2

此种写法能计算出结果,但隐含了自动拆箱装箱,浪费了性能。为此引入了三个针对原始类型的流接口来解决这个问题,分别是:IntStream, DoubleStream和LongStream,分别将流中的元素转化为int, double和long,从而避免了暗含的自动装箱成本。

常见操作如下:

映射到数值流

将流转化为特定版本数值流的方法如下:mapToInt, mapToDouble和mapToLong。转化为数值流后可使用一些快捷方法完成计算:

// 更快捷的计算总卡路里
int sum = menu.stream().mapToInt(Dish::getCalories).sum();

// 还支持如max, min, average等常见方法
  • 1
  • 2
  • 3
  • 4
转化回对象流

数值流类型是IntStream, DoubleStream和LongStream, 某些场景下又想转化回对象流Stream<T>,可执行如下操作:

IntStream i = menu.stream().mapToInt(Dish::getCalories);

// return to Stream<T>
Stream<Integer> s = i.boxed();
  • 1
  • 2
  • 3
  • 4
默认值OptionalInt

对于数值流,也有三个Optional特殊版本,分别是OptionalInt, OptionalDouble和OptionalLong。

例如:要找到IntStream中的最大值,可以调用max方法,会返回一个OptionalInt:

OptionalInt i = menu.stream().mapToInt(Dish::getCalories).max();
int max = i.orElse(1);
  • 1
  • 2
数值范围生成流

IntStream.range(start, end): 可用来生成数值范围序列。
IntStream.rangeClosed(start, end): 可用来生成数值范围序列。

两者的区别是:range不包含end, rangeClosed包含end。

示例:

long total = IntStream.range(0, 100).filter(e -> e % 2 == 0).count();
System.out.println(total);
// result: 50

long total = IntStream.rangeClosed(0, 100).filter(e -> e % 2 == 0).count();
System.out.println(total);
// result: 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
由值创建流

示例:

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); stream.map(String::toUpperCase).forEach(System.out::println);
  • 1

可初始化一个空流

Stream<String> emptyStream = Stream.empty();
  • 1
由数组创建流(常用)
Arrays.stream(new String[]{"a", "b", "c"})
  • 1
由文件生成流

java.nio.file.Files有很多静态方法支持返回一个流。如下面的示例用于返回一个文本文件中不同单词的个数。

/**
 * 演示从文本文件获取流,做相应处理
 */
public class FileStreamSample {

    public static void main(String[] args) {
        // Stream接口天生实现了AutoCloseable接口,所有可以使用try...with...resource语法
        try(Stream<String> lines = Files.lines(Paths.get("/Users/liupeijun/git/learn/1.txt"))) {
            // 使用流的flatMap方法扁平化流
            long count = lines.flatMap(e -> Arrays.stream(e.split(" "))).distinct().count();
            System.out.println(count);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
由函数生成流:创建无限流

StreamAPI提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generte来创建无限流。它们都是按需创建值,如果不加限制可以一直计算下去,所有一般都要加上 限制limit(n),以避免打印无穷多个值。

iterate
示例如下:

// 生成一个从0开始的偶数序列流,限制最大生成100个序列
Stream.iterate(0, n -> n + 2).limit(100).forEach(System.out::println);

// result: 0, 2, 4, 6, ..., 198
  • 1
  • 2
  • 3
  • 4

iterate第一参数是初始值,第二个参数是UnaryOperator<T>类型。

一个实用示例:生成斐波那契数列
格式是:[[0, 1], [1, 2], [2, 3], [3, 5] …]

public class FibonacciSample {

    public static void main(String[] args) {
        Stream.iterate(new long[]{0, 1}, t -> new long[]{t[1], t[0] + t[1]})
                .limit(80)
                .forEach(e -> System.out.println("[" + e[0] + "," + e[1] + "]"));
    }
}
//result:
[0,1]
[1,1]
[1,2]
[2,3]
[3,5]
[5,8]
[8,13]
[13,21]
[21,34]
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

generate
也可以按需生成一个无限流,但不是依次对每个新生成的值应用函数,它接收一个Supplier<T>提供新值,如:

Stream.generate(Math::random)
          .limit(5)
          .forEach(System.out::println);
// result:
 0.9410810294106129
 0.6586270755634592
 0.9592859117266873
 0.13743396659487006
 0.3942776037651241

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

具体generate的进一步应用,这里不再展开。

小结

本节系统学习了如何使用流,能更高效的处理集合了。总结一下知识点:
1)可以使用filter, distinct, skip,limit对流做筛选和切片
2)可以使用map和flatMap做映射和流的扁平化
3)可以使用findFirst和findAny查找流中元素
4)可以使用allMatch, anyMatch, noneMatch查询是否匹配
5)可以利用map-reduce模式实现流元素的归约,如求和,最大,最小等
6)区分有状态操作(reduce, sorted, distinct)和无状态操作(map, filter)
7) 从数组,集合,文件创建流;无限流(iterate,generate)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/367355
推荐阅读
相关标签
  

闽ICP备14008679号