Java 8 Stream 使用

  Stream的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果

  流的流水线背后的理念类似于构建器模式。 在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)

筛选和切片

  • filter方法,过滤
      该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如下来代码所示,筛选出所有素菜,创建一张素食菜单:
    List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
  • distinct方法,去重
      distinct就像在SQL里去重复一样,但要判断Java对象是否重复,需要hashCode和equals方法实现。
  • limit方法,截短流
      该方法会返回一个不超过给定长度的流
  • skip方法,跳过n个元素
      该方法返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。

映射

  • map方法,映射
      它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素,使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去修改。
    List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());这段代码里,menu.stream()返回是的是Streammenu.stream().map(Dish::getName)经过map映射后,返回的是Stream,也就是Stream里的元素,由Dish映射成String。
    List<Integer> dishNameLengths = menu.stream().map(Dish::getName).map(String::length).collect(toList());map可以多次映射,元素为Dish的流,通过Dish的getName映射成String流,再通过String的length应射成Integer流。

扁平化

  • flatMap方法,扁平化
      对Stream的操作,前面的筛选、切片和映射,都很容易理解,而扁平化则逐个分析。先列个例子:
    对于一张单词 表 , 如 何 返 回 一 张 列 表 , 列 出 里 面 各 不 相 同 的 字 符 呢 ? 例 如 , 给 定 单 词 列 表[“Hello”,”World”],你想要返列表[“H”,”e”,”l”, “o”,”W”,”r”,”d”]。
      你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。第一个版本可能是这样的:
    words.stream().map(word -> word.split("")).distinct().collect(toList())
    word -> word.split("")这个函数,它返回的是String []数组,所以最终映射的结果是一个List<String []>的列表,它的每一个元素是String []。

使用Arrays.stream()
  你需要一个Stream<String>,而不是Stream<String []>。有一个叫作Arrays.stream()的方法可以接受一个数组并产生一个Stream。
String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
  把它应用到流水线上:
words.stream().map(word -> word.split("")).map(Arrays::stream).distinct().collect(toList());
  在map(word -> word.split(""))映射成Stream<String []>map(Arrays::stream)继续映射成Stream<Stream<String>>与期望的结果相违背。

使用flatMap()
List<String> uniqueCharacters =words.stream().map(w ->
w.split("")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
  使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。一言以蔽之, flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

匹配和查找

  • anyMatch方法,至少匹配一个元素
  • allMatch方法,所有都匹配
  • noneMatch方法,所有都不匹配

  前面3个方法,返回的都是boolean型,用于做元素匹配,如果要返回匹配的元素值可以用以下两个方法:

  • findAny方法,返回当前流中的任意元素
    Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
      Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在,Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形。
  • findFirst方法,查找第一个元素
      为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

规约

  前面几个操作中筛选、映射、匹配和查找有点类似SQL语法中的select xx from xxx where xx。这些操作都只是针对每一条数据的处理,还记得SQL中还有count、sum、max、min等复合函数的操作,这些复合函数在Stream称为规约,对应就是reduce方法。
reduce方法,规约

  • 对元素求和
    int sum = numbers.stream().reduce(0, (a, b) -> a + b);,如何理解reduce方法呢?reduce接受两个参数,一个初始值,这里是0,一个BinaryOperator来将两个元素结合起来产生一个新值。相当于每次循环遍历,把当前循环的元素当做b,把上一次循环运算的返回值当做a,这两个作为参数,传入到lambda函数,又继续运算进入下一个循环。。

    1
    2
    3
    4
    5
    T result = identity;
    for (T element : this stream) {
    result = accumulator.apply(result, element);
    }
    return result;

      对所有元素求乘积原理跟求和一样,把lambda函数换成(a , b) -> a * b
      reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象。
    Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。

  • 最大值和最小值
    Optional<Integer> max = numbers.stream().reduce(Integer::max);求最大值
    Optional<Integer> min = numbers.stream().reduce(Integer::min);求最小值

  • 求元素个数
    long count = menu.stream().count();

数值流

  上面那个求和的方法,用的是reduce(0, (a, b) -> a + b),为什么不是直接写成sum()岂不是更好?Stream并不是没有提供sum方法,而是对于整型的Stream求sum是有意义的,但对于其他类型的求和就完全没有意义,比如餐单型的Stream。还好Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream、 DoubleStream和LongStream。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max,最小的元素min。

  • 映射到数值流
    int calories = menu.stream().mapToInt(Dish::getCalories).sum();这里的 mapToInt返回的是IntStream(而不是一个Stream)然后就可以调用IntStream接口中定义的sum方法,对卡路里求和了,包括其他的max、 min、 average等。
  • 转换回对象流
    Stream<Integer> stream = intStream.boxed();boxed方法会把IntStream转成Stream
  • 默认值OptionalInt
    OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();,max()方法返回的是OptionalInt,那是因为如果一个IntStram,里面一个元素都没有,它肯定也是没有最大值,而不是返回0。OptionalInt跟上面Optional很像,可以判断返回的这个值是不是空的。
      IntStream还有几个静态方法来初始化一个IntStream,例如IntStream evenNumbers = IntStream.rangeClosed(1, 100)初始化一个IntStream,里面的元素从1到100。IntStream evenNumbers = IntStream.of(1, 2,3,...),用整型数组初始化IntStream。

构建一个Stream

  由集合生成一个Stream,如Collection.stream()这个是比较常用的,另外还有一些创建Stream的方法

  • 由数组创建流
    int[] numbers = {2, 3, 5, 7, 11, 13};
    int sum = Arrays.stream(numbers).sum();
  • 由值创建流
    Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
  • 由文件生成流
      java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines它会返回一个由指定文件中的各行构成的字符串流。
    Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())
  • 由函数生成流:创建无限流
      Stream API提供了两个静态方法来从函数生成流: Stream.iterateStream.generate
    Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,以此类推。
    Stream.generate(Math::random).limit(5).forEach(System.out::println);这段代码将生成一个流,其中有五个0到1之间的随机双精度数。

参考资料

  • 《Java 8 in Action》