什么是Stream?

Stream(流)是一个来自数据源的元素队列,并且支持聚合操作

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

Stream的本质

Stream的本质不是一个数据结构,应该说他是一种规则,它规定了这个流应该进行一番怎样的操作,最后得到一个具体的结果。
所以Stream API有分为中间操作和结束操作

  • 中间操作: 一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据操作,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,就是说,仅仅调用到这类方法,制定流的遍历规则。还没有开始真正便利流。
  • 结束操作:一个流只能有一个结束操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。结束操作的执行,才会真正开始流的遍历,并按照之前的中间操作制定的规则去遍历流,并生成一个结果。
操作类型接口方法
中间操作concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操作allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

Stream的优点:

  • 可复合,中间操作都是可以任意搭配的,对数据的处理灵活性更高。
  • 可并行,程序的效率更高。
  • 声明性,中间操作既是对数据的处理的声明,使代码更加简洁且利于理解。

Stream的缺点:

  • 调试代码不友好,因为中间操作只是制定规则,没有进行实际的操作,所以无法进行Debug
  • 对于不懂的人来说Stream 看不懂

Stream的简单示例

// 员工信息表(Employee类可参照上篇Lambda表达式的文章获取)
List<Employee> employees = Arrays.asList(
        new Employee("张三",23,9999.99),
        new Employee("李四",38,7777.77),
        new Employee("王五",55,3333.33),
        new Employee("赵六",18,6666.66)
);

// 获得员工列表中所有年龄大于35的员工的姓名
// map和filter
@Test
public void test1() {
    List<String> names = employees.stream()
            .filter(e -> e.getAge() > 35)
            .map(Employee::getName)
            .collect(Collectors.toList());

    names.forEach(System.out::println);
}

在上面的示例中,先是使用集合的.stream()方法将一个List转化为流,然后使用中间操作制定这个流的处理规则.filter(e -> e.getAge() > 35),过滤出所有年龄大于35岁的员工,.map(Employee::getName)获取这些年龄大于35的员工的姓名,再使用结束操作处理这个流得到员工列表中所有年龄大于35的员工的姓名names列表。

Stream的复杂API reduce示例

reduce方法有三个重载的方法,方法签名如下

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

第一个方法签名的reduce示例

@Test
public void test2() {

    Double sumSalary1 = employees.stream()
            .map(Employee::getSalary)
            .reduce((a,b) -> a+b )
            .get();

    Double sumSalary = employees.stream()
            .map(Employee::getSalary)
            .reduce(Double::sum)
            .get();

    System.out.println(sumSalary);
    System.out.println(sumSalary.equals(sumSalary1));
}

上面的代码实现了对员工的工资累加。lambada表达式的a参数是表达式的执行结果的缓存,也就是表达式这一次的执行结果会被作为下一次执行的参数,而第二个参数b则是依次为stream中每个元素。如果表达式是第一次被执行,a则是stream中的第一个元素。
其中(a,b) -> a+b可以简写为Double::sum,是因为Double的sum方法的实现就是a+b。

第二个方法签名的reduce示例

@Test
public void test3() {
    Double sumSalary = employees.stream()
            .map(Employee::getSalary)
            .reduce(100.0,(a, b) -> a + b );

    System.out.println(sumSalary);
}

与第一个签名的实现的唯一区别是它首次执行时表达式第一次参数并不是stream的第一个元素,而是通过签名的第一个参数identity来指定。
其实这两种实现几乎差别,第一种比第一种仅仅多了一个字定义初始值罢了。 此外,因为存在stream为空的情况,所以第一种实现并不直接方法计算的结果,而是将计算结果用Optional来包装,我们可以通过它的get方法获得一个Integer类型的结果,而Integer允许null。第二种实现因为允许指定初始值,因此即使stream为空,也不会出现返回结果为null的情况,当stream为空,reduce会直接把初始值返回。

第三个方法签名的reduce示例

第三种签名的用法相较前两种稍显复杂,由于前两种实现有一个缺陷,它们的计算结果必须和stream中的元素类型相同,如果上面的代码工资用int表示,stream中的类型为int,那么计算结果也必须为int,这导致了灵活性的不足,甚至无法完成某些任务, 比入我们要对对一个一系列int值求和,但是求和的结果用一个int类型已经放不下,必须升级为long类型,此实第三签名就能发挥价值了,它不将执行结果与stream中元素的类型绑死。

@Test
public void test4() {
    List<Integer> numList = Arrays.asList(Integer.MAX_VALUE,Integer.MAX_VALUE);
    long result = numList.stream().reduce(0L,(a,b) ->  a + b, (a,b)-> 0L );
    System.out.println(result);
}

当然这只是其中一种应用罢了,犹豫拜托了类型的限制我们还可以通过他来灵活的完成许多任务,比入将一个int类型的ArrayList转换成一个String类型的ArrayList

@Test
public void test5(){
    List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6);
    ArrayList<String> result = numList.stream().reduce(new ArrayList<String>(), (a, b) -> {
        a.add("element-" + b);
        return a;
    }, (a, b) -> null);
    System.out.println(result);
}