1. 流操作的整体步骤
  2. 中间操作 - 筛选和切片
  3. 中间操作 - 映射
  4. 查找
  5. 终端操作 - 归约
  6. 基本类型特化的流
  7. 其他应用

流可以说是操作集合的新方法, 结合前边的函数式编程, 可以更高效的编写代码, 可以看成是高级的迭代器.

流有几个术语:

  1. 元素序列, 就是一个流对象提供的接口, 可以访问到一组元素.
  2. 源, 流会使用一个提供数据的源, 由其他集合生成的流, 排序情况(或者无序)与原集合一致
  3. 数据处理操作, 这个类似于SQL语句, 对每一个流中的元素进行一些操作, 比如filter, map, reduce, find, match, sort等, 都是函数式编程中的常见操作.
  4. 流水线, 很多流操作不会变更原来的流, 而是返回一个新的流, 这和操作集合对象有些不同. 这些操作可以串起来成为一个流水线
  5. 内部迭代, 很多流的数据处理操作暗含了迭代, 无需编写显式的迭代代码.

下边这个例子可以说明上边的术语:

public static void main(String[] args) {

    //一个Dish的List
    List<Dish> dishes = Dish.getMenu();

    //转换成一个Dish的流对象源
    Stream<Dish> dishes1 = dishes.stream();

    List<String> newDishes = dishes.stream()
            //数据处理操作filter, 这个操作生成了一个新的流
            .filter(dish -> dish.getCalories() > 300)
            //map方法表示接受一个lambda, 将元素转换成其他形式, map的意思表示对每个元素应用其中的lambda表达式, 收集应用后的结果
            //map操作也生成了一个新的流
            .map(dish -> dish.getName())
            //获取前三个, 生成一个新的流
            .limit(3)
            //收集成一个列表, 这个是终结流的操作, 每个流必须有一个终结操作, 否则之前确定的动作不会执行
            .collect(Collectors.toList());

    System.out.println(newDishes);
}

流只能遍历一次, 之后这个流就会被消费掉, 需要从原始数据源那里获取一个新的流来遍历一次. 像集合之类可以反复调用方法来生成流, 但是像IO就没戏了.

public class Consume {

    public static void main(String[] args) {
        List<String> title = Arrays.asList("Java8", "in", "action");

        Stream<String> s = title.stream();
        //这一行工作正常
        s.forEach(System.out::println);
        //想再次输出, 就会抛出流已经消费的异常
        s.forEach(System.out::println);
    }
}

流操作的整体步骤

总体上来说, 流的操作分为两大类:

  1. 中间操作: 返回一个流的操作. 可以互相连接, 在连接的过程中不会发生实际操作.
  2. 终端操作: 会从流生成一个结果, 并且消费掉这个流. 在执行了终端操作之后, 所有的中间操作才会启动.

所以使用流就是三步:

  1. 获取流, 常见的集合都提供了.stream()方法, 此外Stream库中也有生成流的方法
  2. 中间操作, 常见的有filter, map, limit, sorted, distinct
  3. 终端操作, 常见的有forEach, count, collect, 其中collect是将流规约, 其中又有很多操作

筛选和切片

filter的定义如下:

Stream<T> filter(Predicate<? super T> predicate);

可见filter方法接收的是一个Predicate接口, 这个接口已经很熟悉了, 就是根据返回的布尔值来过滤为true的元素.

需要注意的是, 调用过filter之后, 得到了符合条件的一个新流, 但是流中的元素并没有改变类型. 所以一个Stream<T>在过滤之后, 还是一个Stream<T>.

还有一个distinct()方法, 没有参数, 这个方法调用之后, 会根据流中元素的的hashCode和equals方法, 来去掉重复的元素, 仅保留不同的元素, 然后生成一个新流, 元素类型依然不会改变.

如果只需要部分元素, limit(n)可以只从流中获取n个元素, 如果流有序, 则就是最开始的n个, 如果流无序, 则获取的是无序的n个元素.

如果需要跳过部分元素, 则可以使用skip(n). 例如:

public static void main(String[] args) {
    List<Dish> dishes = Dish.getMenu();

    List<Dish> dishes1 = dishes.stream().filter(dish -> dish.getCalories() > 200).limit(2).collect(Collectors.toList());
    System.out.println(dishes1);

    List<Dish> dishes2 = dishes.stream().filter(dish -> dish.getCalories() > 400).skip(2).collect(Collectors.toList());
    System.out.println(dishes2);
}

筛选, 切片和跳过的最大特点就是不改变其中的元素类型, 只是将流变得不会多于原来的流.

映射

映射对于流来讲, 就是把一个流中的元素, 转换成另外一个新流中的经过处理的元素, 元素的类型可能改变, 也可能不改变, 但通常会改变

最典型的映射就是map()方法:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

这是定义在Stream类中的一个泛型方法, 接受的是一个Function<T, R>接口对象, 也就是接受T返回R类型的函数式接口, 会使用这个函数对流中的元素进行操作.

而每一个元素被操作之后的结果, 会组成一个新流, 这就是map的特点所在:

List<String> dishNames = dishes.stream()
        //第二种方法引用, 即任意实例的方法
        .map(Dish::getName)
        //组成一个List,此时List的类型是什么呢, 是Dish::getName返回的类型,也就是String
        .collect(Collectors.toList());

将一个流中的元素进行操作并且转换成另外一个类型的流是比较直观的. 扁平化就是不是很容易了, 扁平化我个人这么理解, 就是流中的每个元素都是流, 如果仅仅使用map, 那流中的元素还是流, 使用flatmap, 就会把流中的每个流都拆成其中的元素, 再把这些元素拼起来成新的流.

将List<String> s = ["Hello","World"]展开成["H","e","l", "o","W","r","d"], 用map的话, 一定要注意返回类型:

public static void main(String[] args) {

    List<String> ss = Arrays.asList("Hello", "world!");

    ss.stream().map(s -> s.split("")).forEach(System.out::println);
    ss.stream().map(s -> s.split("")).forEach(strings -> System.out.println(Arrays.toString(strings)));

}

这个打印的结果是什么呢, 注意map的操作, 将列表转换成流后, 流中的元素类型是原来集合中的元素类型, 也就是一个个字符串.

s -> s.split("") 的返回值是一个String数组, 这就意味着, 在使用完map之后, 流中的元素类型从字符串变成了字符串数组, 个数不变, 还是两个.

所以上边程序执行的结果是:

[H, e, l, l, o]
[w, o, r, l, d, !]
[Ljava.lang.String;@783e6358
[Ljava.lang.String;@17550481

可以看到, map是无法改变元素的个数的. 很显然使用map是不行的. 这个时候就需要使用flatmap了.

fflatmap的定义如下:

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

首先这是一个泛型方法, 关键是其中的参数, 可以简化成 Function<T, Stream<R>的一个函数接口, 这个接口的意思表示接受一个T类型, 返回一个流对象. 这个函数对象表示如何将流中每个元素变成一个流, flatmap本身做的事情就是使用这个函数展开流, 再扁平化.

再简单一点说, flatMap接受的匿名对象的方法是用来产生一个流的, flatMap会把这个流展开, 当成当前流的内容. 虽然有点拗口, 但是确实需要仔细体会.

我可以理解成, 就是把每个元素当成一个流, 展开后合并起来成为一个流, 再来试试:

List<String> uniqueCharacters =
        ss.stream()
                //生成了一个流元素是字符串数组的流
                .map(w -> w.split(""))
                //传入的函数对象是将数组变成流
                //flatMap使用这个函数将每个数组变成流, 再将流中的每个元素合并到当前的流中
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());

这样就可以操作了, 所以要注意flatMap和map接受的lambda表达式的意义是不同的.

书上有一道题目, 写的和书上不一样, 但是是经过了自己的思考:

给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]:

import java.util.Arrays;

public class Ex52 {

    public static void main(String[] args) {
        int[] a1 = {1, 2, 3};
        int[] a2 = {3, 4};

        //想法, 用一个流和另外一个流生成对象, 再扁平化展开就可以了, 试验一下吧
        //这是将a1和一个数字10转换成一个Tuple流的代码, 现在要解决的就是把10改成来自于另外一个流
        Arrays.stream(a1).mapToObj(operand -> new Tuple<Integer>(operand, 10)).forEach(System.out::println);

        //对于a1中的每一个int, 返回一个Arrays.stream(a2).mapToObj(last -> new Tuple<Integer>(first, last))
        //Arrays.stream(a2).mapToObj(last -> new Tuple<Integer>(first, last)) 是针对a2中的每个 int 将其换成一个Tuple的流.

        //这样执行完之后, 实际上就得到了一个所有的元素都是流的流对象
        Arrays.stream(a1).mapToObj(first ->
                Arrays.stream(a2).mapToObj(last -> new Tuple<Integer>(first, last))
        ).forEach(s -> System.out.println(s.getClass()));

        //然后就需要展开了, 由于其中每一个流已经是流了, flatMap这里无需做任何处理, 就是 s -> s
        Arrays.stream(a1).mapToObj(first ->
                Arrays.stream(a2).mapToObj(last -> new Tuple<Integer>(first, last))
        ).flatMap(s -> s).forEach(System.out::println);

    }
}

原书的解法确实棒, 利用了flatMap, 将生成流的代码直接写在flatMap之内.

查找

相比在集合中查找返回的boolean, 要先看一个Java 8的新类叫做Optional<T>.

这个类位于java.util.Optional中, 是一个容器类, 代表一个值存在或者不存在, 这样不容易出现null问题.

这个类有几个实例方法:

  1. isPresent(), 是否包含值, 包含返回true
  2. ifPresent(Consumer T consumer), 在值存在的时候对其执行consumer函数. 这个Consumer接口已经学过了, 接受T类型返回void.
  3. T get(), 存在值的时候返回值, 否则抛出NoSuchElement异常.
  4. T orElse(T other), 如果不存在的时候, 返回传入的other作为默认值, 如果存在, 等于 T get().

然后就可以来看看查找方法了, 首先是一些找到或者找不到的方法:

  1. boolean anyMatch(Predicate<? super T> predicate); 根据条件只要有一个匹配, 就返回true
  2. boolean allMatch(Predicate<? super T> predicate); 如果全部匹配, 才返回true
  3. boolean noneMatch(Predicate<? super T> predicate); 如果全部不匹配, 才返回true

这些方法中都使用了短路, 所以不必处理完全部的流, 就可以得到结果(假如满足条件的话).

之后是查找某个具体元素的方法, 这些方法返回的都是Optional对象:

  1. findFirst(), 找有顺序的第一个元素, 如果流无序, 则不确定是哪一个
  2. findAny(), 找有顺序的第一个元素, 如果流无序, 则不确定是哪一个

终端操作 - 归约

前边除了查找之外, 都是中间操作, 只是中间操作的话, 流的操作没有执行, 也无法得到结果. 只有执行了终端操作, 流才能转换成实际可以使用的数据.

这里的归约指的是reduce方法, 还不是collect()方法. collect()要单独来学习.

之前写过一篇Reduce的三种重载, reduce的基础使用就不再赘述了.

由于最后是将流规约为一个数字, 所以比较最大最小之类的也可以来操作了, 比如:

//这里是静态方法引用
Optional<Integer> min = numbers.stream().reduce(Integer::min);

特化基础类型流

和之前的lambda表达式类似, 也有类型特化的流来避免不必要的开销.

Java 8引入了三个原始类型特化流接口:IntStream、DoubleStream和LongStream, 而且每个接口都带了一些特有的简化归约方法.

还可以将一个流映射成特化的基础类型流, 常用方法是mapToInt、mapToDouble和mapToLong, 会返回上边的三个类型的流接口对象.

而数值流还可转换成装箱后的Stream流对象, 只要调用box()方法即可.

对于Optional, 也提供了OptionalInt、OptionalDouble和OptionalLong这三个对应的版本.

其他应用

其他应用有如下几个:

  1. 两个可以用于IntStream和LongStream的静态方法,帮助生成范文中的数:range和rangeClosed. 两者的区别是第一个方法是左闭右开, 第二个是两端都闭口.
  2. 从值创建流, Stream.of("Java 8 ", "Lambdas ", "In ", "Action");, Stream 的 of 静态方法可以支持基础类型和很多内置的类型
  3. 从数组创建流, Arrays.stream(numbers), 从数组创建流, 一样也支持很多内置类型
  4. 从文件创建流, java.nio.file.Files中的很多静态方法都能够生成流, 比如File.lines(). NIO在1.4引入, 但随着1.8 的到来也加入了流要素, 估计也要看看才行.
  5. 从函数生成流, Stream.iterate静态方法接受一个初始值和一个依次应用在初始值上的函数对象, 生成一个流, 这个流是无限流. 一般需要限制大小
  6. 创造新的值, Stream.generate,静态方法接受一个Supplier<T>, 每次根据Supplier生成流, 也是无限流.