【Java 8实战笔记】使用流

章节内容

  • 筛选、切片和匹配
  • 查找、匹配和规约
  • 使用数值范围等数值流
  • 从多个源创建流
  • 无限流

筛选和切片

用谓词筛选

Stream接口支持filter方法。该操作会接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流。

例如:

List<Dish> vegetarianMenu = menu.stream()
                                .filter(Dish::isVegetarian)
                                .collect(toList());
筛选各异的元素

流支持distinct方法,它会返回一个元素各异的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。

List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream()
        .filter(i -> i%2 ==0)
        .distinct()
        .forEach(System.out::println);
截短流

流支持limit(n)方法,该方法会返回一个不超过给定长度n的流。

例如:

List<Dish> dishes = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .limit(3)
            collect(toList());
跳过元素

流还支持skip(n)方法,返回一个扔掉前n个元素的流。如果流元素不足n个则返回一个空流。

例如:

List<Dish> dishes = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .skip(2)
            collect(toList());

映射

一个常见的数据处理套路就是从某些对象中选择信息。在SQL里,可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。

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

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。

例如:

//提取菜肴名称
List<String> dishNames = menu.stream()
            .map(Dish::getName)
            .collect(toList());
流的扁平化

对于一个单词表,如何返回一张列表,列出里面各不相同的字符?例如给定单词列表["Hello","World"],要返回列表["H","e","l","o","W","r","d"]。

最开始的版本可能是这样的:

words.stream()
    .map(word -> word.split(""))
    .collect(toList());

这样做的问题在于,传递给map方法的Lambda为每个单词返回了一个String[]。因此map返回的流实际上是Stream<String[]>类型的。而真正想要的是用Stream<String>来表示一个字符流。

  1. 尝试使用map和Arrays.stream()

首先,需要一个字符流,而不是数组流。有一个叫做Arrays.stream()的方法可以接受一个数组并产生一个流,例如

String[] arraysOfWords = {Goodbye", "World"};
Stream<String>  streamOfWords = Arrays.stream(arrayOfWords); 

将它用在前面的流水线里

words.stream()
    .map(word -> word.split(""))
    .map(Arrays::stream)
    .distinct()
    .collect(toList());

当前的解决方案仍然搞不定,因为现在得到的是一个流的列表(因为先是把每个单词转换成一个字母数组,然后把每个数组变成了独立的流

2.使用flatMap

可以像下面这样使用flatMap来解决这个问题:

List<String> uniqueCharacters =  
        words.stream()
            .map(word -> word.split(""))
            .map(Arrays::stream)
            .distinct()
            .collect(toList());

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map()时生成的单个流都被合并起来,即扁平化为一个流。
简而言之flatMap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来称为一个流。

测验:

1.给定两个数字列表[1,2,3]和[3,4],返回综合能被3整除的数对。

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

查找和匹配

另一个常见的数据处理套路是看数据集中某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供这样的工具。

检查谓词是否至少匹配一个元素(anyMatch)
if(menu.stream().anyMatch(Dish::isVegetarian)){
    System.out.println("The menu is vegetarian friendly");
}

anyMatch方法返回一个boolean值,因此是一个终端操作

检查谓词是否匹配所有元素(allMatch)
menu.stream().allMatch( d -> d.getCalories() < 1000);
检查谓词是否与所有谓词都不匹配(noneMatch)
menu.stream().noneMatch( d -> d.getCalories() >= 1000);
查找元素

findAny方法将返回当前流的任意元素:

Optional<Dish> dish = 
        menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();

但代码里的Optional是什么?

Optional简介

Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。Java 8通过引入Optional<T> 来避免返回众所周知的容易出问题的null。

Optional里有几种 可以迫使你显式地检查值是否存在或处理值不存在情形的 方法。

  • isPresent()将在Optional包含值的时候返回true,否则返回false
  • isPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。
  • T get() 会在值存在时返回值,否则抛出一个NoSuchElement异常。
  • T orElse(T other)会在值存在时返回值,否则返回一个默认值。

例如在前面的findAny代码中你需要显式地检查Optional对象中是否存在一道菜可以访问其名称:

menu.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .ifPresent(d -> System.out.println(d.getName());
查找一个元素

有些流有一个出现顺序来指定流中项目出现的逻辑顺序。对于这种流,想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findAny,它们的区别在于并行上的限制,如果不关心返回的元素是哪个,就用findAny。


归约

如何把一个流中的元素组合起来并表达更复杂的查询?如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作。

元素求和

先看看如何使用for-each循环来对数字列表中的元素求和:

int sum = 0;
for(int x: numbers){
    sum += x;
}

这段代码中有两个参数:

  • 总和变量的初始值
  • 将列表中所有元素结合在一起的操作

reduce对上面这种重复应用的模式做了抽象,可以像下面这样对流中所有的元素求和:

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

reduce接受两个参数:

  • 一个初始值
  • 一个BinaryOperator<T>来将两个元素结合起来产生一个新值。

还可以使用方法引用让这段代码更简洁。在Java 8中,Integer类现在有了 一个静态的sum方法来对两个数求和:

int sum = numbers.stream().reduce(0, Integer::sum);

无初始值

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象(考虑到流中没有任何元素,无法返回其和的情况):

Optional<Integer> sum = numbers.stream().reduce((a, b) -> a+b);
最大值和最小值

利用reduce来计算最大值和最小值

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

数值流

Stream API还提供了原始类型流特化,专门支持处理数值流的方法

原始类型流特化

Java 8引入了三个原始类型特化流来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中元素特化为int、long和double,可以避免暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。

  1. 映射到数值流

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapLong。这些方法和前面的map方法的工作方式一样,只不过返回的是一个特化流,而不是Stream<T>。例如可以像下面这一行用mapToInt对menu中的卡路里求和:

int calories = menu.stream()
                .mapToInt(Dish::getCalories)
                .sum();

mapToInt会返回一个IntStream,然后就可以调用IntStream接口中定义的sum方法对卡路里求和。如果流是空的,sum默认返回0。
IntStream还支持其他的方法,如max、min、average等。

  1. 转换回对象流

要把原始流转换成一般流,可以使用boxed方法,如下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
  1. 默认值OptionalInt

求和很容易,因为它有一个默认值0。但是如果要计算IntStream的最大元素,默认值0是错误的结果。如何区分没有元素的流和最大值真的是0的流?Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。

例如要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt,如果没有最大值的话,就可以显示处理OptionalInt去定义一个默认值了:

OptionalInt maxCalories = menu.stream()\
                .mapToInt(Dish::getCalories)
                .max();

int max = maxCalories.orElse(1);
数值范围

比如生成1到100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range生成的范围不包含结束值,而rangeClosed包含结束值。

IntStream evenNumbers = IntStream.rangeClosed(1, 100)
                              .filter(n -> n%2 == 0);

System.out.println(evenNumbers.count());

构建流

接下来将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流。

由值创建流

使用静态方法Stream.of通过显式值创建一个流。它可以接受任意数量的参数。

例如:

Stream<String> stream  = stream.of("Java 8","In","Action");

可以使用empty得到一个空流:

Stream<String> emptyStream  = stream.empty();
由数组创建流

可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。

int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum();
由文件生成流

java.nio.file.Files中的很多静态方法都会返回一个流。例如,Files.lines方法会返回一个由指定文件中的各行构成的字符串流。

统计一个文件中有多少各不相同的词:

long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"),charset.defaultCharset())){
        uniqueWords = lines.flatMap(line -> Arrays.stream(line.split("")))
                          .distinct()
                          .count();
}
catch(IOException e){
}

上面的代码使用Files.lines得到一个流,其中的每个元素都是文件中的一行。然后对line调用split方法将行拆分成单词。最后把distinct和count方法链接起来,统计出各不相同的单词的个数。

由函数生成流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流(不像从固定集合创建的流那样有固定大小的流)。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说应该使用limit(n)对无限流加以限制,避免打印无穷多个值。

  1. 迭代
Stream.iterate(0, n -> n+2)
        .limit(10)
        .forEach(System,out::println);

iterate接受一个初始值。还有一个一次应用在每个产生的新值上的Lambda(UnaryOperator<T>类型)。此操作将生成一个无限流 ----- 这个流没有结尾,因为值是按需计算的,因此这个流是无界的。

  1. 生成

与iterate方法不同的是,generate方法不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。

Stream.generate(Math:random)
            .limit(5)
            .forEach(System.out::println)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,280评论 19 139
  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,531评论 0 4
  • 概要 流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了: 现...
    浔它芉咟渡阅读 1,552评论 1 2
  • Java8 in action 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式...
    铁牛很铁阅读 1,288评论 1 2
  • 筛选和切片 映射 查找和匹配 规约 数值流 构建流 欢迎访问本人博客:http://wangnan.tech 筛选...
    GhostStories阅读 959评论 0 3