最近在公司写业务的时候,突然想不起来Stream中的累加应该怎么写?

无奈只能面向谷歌编程,破费了我贵重的三分钟之后,学会了,很简略。

自从我用上JDK8当前,Stream就是我最罕用的个性,各种流式操作用的飞起,然而这次事当前我突然感觉Stream对我真的很生疏。

可能大家都一样,对最罕用到的货色,也最容易将其疏忽,哪怕你要筹备面试预计也必定想不起来要看一下Stream这种货色。

不过我既然留神到了,就要从新梳理一遍它,也算是对我的整体常识体系的查漏补缺。

花了很多功夫来写这篇Stream,心愿大家和我一块重新认识并学习一下Stream,理解API也好,理解外部个性也罢,怕什么真谛无穷,进一步有进一步的欢喜。

在本文中我将Stream的内容分为以下几个局部:

初看这个导图大家可能对转换流操作和终结流操作这两个名词有点蒙,其实这是我将Stream中的所有API分成两类,每一类起了一个对应的名字(参考自Java8相干书籍,见文末):

  • 转换流操作 :例如filter和map办法,将一个Stream转换成另一个Stream,返回值都是Stream。
  • 终结流操作 :例如count和collect办法,将一个Stream汇总为咱们须要的后果,返回值都不是Stream。

其中转换流操作的API我也分了两类,文中会有具体例子阐明,这里先看一下定义,有一个大略印象:

  1. 无状态 :即此办法的执行无需依赖后面办法执行的后果集。
  2. 有状态 :即此办法的执行须要依赖后面办法执行的后果集。

因为Stream内容过多,所以我将Stream拆成了高低两篇,本篇是第一篇,内容翔实,用例简略且丰盛。

第二篇的主题尽管只有一个终结操作,然而终结操作API比较复杂,所以内容也翔实,用例也简略且丰盛,从篇幅上来看两者差不多,敬请期待。


:因为我本机的电脑是JDK11,而且写的时候忘了切换到JDK8,所以在用例中大量呈现的List.of()在JDK8是没有的,它等同于JDK8中的Arrays.asList()

1. 为什么要应用Stream?

所有还要源于JDK8的公布,在那个函数式编程语言热火朝天的时代,Java因为它的臃肿而饱受诟病(强面向对象),社区迫切需要Java能退出函数式语言特点改善这种状况,终于在2014年Java公布了JDK8。

在JDK8中,我认为最大的新个性就是退出了函数式接口和lambda表达式,这两个个性取自函数式编程。

这两个特点的退出使Java变得更加简略与优雅,用函数式反抗函数式,坚固Java老大哥的位置,几乎是师夷长技以制夷。

而Stream,就是JDK8又依靠于下面的两个个性为汇合类库做的 一个类库,它能让咱们通过lambda表达式更简明扼要的以流水线的形式去解决汇合内的数据,能够很轻松的实现诸如:过滤、分组、收集、归约这类操作,所以我愿将Stream称为函数式接口的最佳实际。

1.1 更清晰的代码构造

Stream领有更清晰的代码构造,为了更好的解说Stream怎么就让代码变清晰了,这里假如咱们有一个非常简单的需要:在一个汇合中找到所有大于2的元素

先来看看没应用Stream之前:

        List<Integer> list = List.of(1, 2, 3);                List<Integer> filterList = new ArrayList<>();                for (Integer i : list) {            if (i > 2) {                filterList.add(i);            }        }                System.out.println(filterList);

下面的代码很好了解,我就不过多解释了,其实也还好了,因为咱们的需要比较简单,如果需要再多点呢?

每多一个要求,那么if外面就又要加一个条件了,而咱们开发中往往对象上都有很多字段,那么条件可能有四五个,最初可能会变成这样:

        List<Integer> list = List.of(1, 2, 3);        List<Integer> filterList = new ArrayList<>();        for (Integer i : list) {            if (i > 2 && i < 10 && (i % 2 == 0)) {                filterList.add(i);            }        }        System.out.println(filterList);

if外面塞了很多条件,看起来就变得乱哄哄了,其实这也还好,最要命的是我的项目中往往有很多相似的需要,它们之间的区别只是某个条件不一样,那么你就须要复制一大坨代码,改吧改吧就上线了,这就导致代码里有大量反复的代码。

如果你Stream,所有都会变得清晰易懂:

        List<Integer> list = List.of(1, 2, 3).stream()                .filter(i -> i > 2)                .filter(i -> i < 10)                .filter(i -> i % 2 == 0)                .collect(toList());

这段代码你只须要关注咱们最关注的货色:筛选条件就够了,filter这个办法名能让你分明的晓得它是个过滤条件,collect这个办法名也能看进去它是一个收集器,将最终后果收集到一个List外面去。

同时你可能发现了,为什么下面的代码中不必写循环?

因为Stream会帮忙咱们进行隐式的循环,这被称为:外部迭代,与之对应的就是咱们常见的内部迭代了。

所以就算你不写循环,它也会进行一遍循环。

1.2 不用关怀变量状态

Stream在设计之初就被设计为不可变的,它的不可变有两重含意:

  1. 因为每次Stream操作都会生成一个新的Stream,所以Stream是不可变的,就像String。
  2. 在Stream中只保留原汇合的援用,所以在进行一些会批改元素的操作时,是通过原元素生成一份新的新元素,所以Stream 的任何操作都不会影响到原对象。

第一个含意能够帮忙咱们进行链式调用,实际上咱们应用Stream的过程中往往会应用链式调用,而第二个含意则是函数式编程中的一大特点:不批改状态。

无论对Stream做怎么样的操作,它最终都不会影响到原汇合,它的返回值也是在原汇合的根底上进行计算得来的。

所以在Stream中咱们不用关怀操作原对象汇合带来的种种副作用,用就完了。

对于函数式编程能够查阅阮一峰的函数式编程初探。

1.3 提早执行与优化

Stream只在遇到终结操作的时候才会执行,比方:

        List.of(1, 2, 3).stream()                .filter(i -> i > 2)                .peek(System.out::println);

这么一段代码是不会执行的,peek办法能够看作是forEach,这里我用它来打印Stream中的元素。

因为filter办法和peek办法都是转换流办法,所以不会触发执行。

如果咱们在前面退出一个count办法就能失常执行:

        List.of(1, 2, 3).stream()                .filter(i -> i > 2)                .peek(System.out::println)                .count();

count办法是一个终结操作,用于计算出Stream中有多少个元素,它的返回值是一个long型。

Stream的这种没有终结操作就不会执行的个性被称为提早执行

与此同时,Stream还会对API中的无状态办法进行名为循环合并的优化,具体例子详见第三节。

2. 创立Stream

为了文章的完整性,我思来想去还是加上了创立Stream这一节,这一节次要介绍一些创立Stream的罕用形式,Stream的创立个别能够分为两种状况:

  1. 应用Steam接口创立
  2. 通过汇合类库创立

同时还会讲一讲Stream的并行流与连贯,都是创立Stream,却具备不同的特点。

2.1 通过Stream接口创立

Stream作为一个接口,它在接口中定义了定义了几个静态方法为咱们提供创立Stream的API:

    public static<T> Stream<T> of(T... values) {        return Arrays.stream(values);    }

首先是of办法,它提供了一个泛型可变参数,为咱们创立了带有泛型的Stream流,同时在如果你的参数是根本类型的状况下会应用主动包装对根本类型进行包装:

        Stream<Integer> integerStream = Stream.of(1, 2, 3);        Stream<Double> doubleStream = Stream.of(1.1d, 2.2d, 3.3d);        Stream<String> stringStream = Stream.of("1", "2", "3");

当然,你也能够间接创立一个空的Stream,只须要调用另一个静态方法——empty(),它的泛型是一个Object:

        Stream<Object> empty = Stream.empty();

以上都是咱们让咱们易于了解的创立形式,还有一种形式能够创立一个无限度元素数量的Stream——generate():

    public static<T> Stream<T> generate(Supplier<? extends T> s) {        Objects.requireNonNull(s);        return StreamSupport.stream(                new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);    }

从办法参数上来看,它承受一个函数式接口——Supplier作为参数,这个函数式接口是用来创建对象的接口,你能够将其类比为对象的创立工厂,Stream将从此工厂中创立的对象放入Stream中:

        Stream<String> generate = Stream.generate(() -> "Supplier");        Stream<Integer> generateInteger = Stream.generate(() -> 123);

我这里是为了不便间接应用Lamdba结构了一个Supplier对象,你也能够间接传入一个Supplier对象,它会通过Supplier接口的get() 办法来结构对象。

2.2 通过汇合类库进行创立

相较于下面一种来说,第二种形式更较为罕用,咱们经常对汇合就行Stream流操作而非手动构建一个Stream:

        Stream<Integer> integerStreamList = List.of(1, 2, 3).stream();                Stream<String> stringStreamList = List.of("1", "2", "3").stream(); 

在Java8中,汇合的顶层接口Collection被退出了一个新的接口默认办法——stream(),通过这个办法咱们能够不便的对所有汇合子类进行创立Stream的操作:

        Stream<Integer> listStream = List.of(1, 2, 3).stream();                Stream<Integer> setStream = Set.of(1, 2, 3).stream();

通过查阅源码,能够发先 stream() 办法实质上还是通过调用一个Stream工具类来创立Stream:

    default Stream<E> stream() {        return StreamSupport.stream(spliterator(), false);    }

2.3 创立并行流

在以上的示例中所有的Stream都是串行流,在某些场景下,为了最大化压迫多核CPU的性能,咱们能够应用并行流,它通过JDK7中引入的fork/join框架来执行并行操作,咱们能够通过如下形式创立并行流:

        Stream<Integer> integerParallelStream = Stream.of(1, 2, 3).parallel();        Stream<String> stringParallelStream = Stream.of("1", "2", "3").parallel();        Stream<Integer> integerParallelStreamList = List.of(1, 2, 3).parallelStream();        Stream<String> stringParallelStreamList = List.of("1", "2", "3").parallelStream();

是的,在Stream的静态方法中没有间接创立并行流的办法,咱们须要在结构Stream后再调用一次parallel()办法能力创立并行流,因为调用parallel()办法并不会从新创立一个并行流对象,而是在原有的Stream对象下面设置了一个并行参数。

当然,咱们还能够看到,Collection接口中能够间接创立并行流,只须要调用与stream() 对应的parallelStream()办法,就像我方才讲到的,他们之间其实只有参数的不同:

    default Stream<E> stream() {        return StreamSupport.stream(spliterator(), false);    }    default Stream<E> parallelStream() {        return StreamSupport.stream(spliterator(), true);    }

不过个别状况下咱们并不需要用到并行流,在Stream中元素不过千的状况下性能并不会有太大晋升,因为将元素扩散到不同的CPU进行计算也是有老本的。

并行的益处是充分利用多核CPU的性能,然而应用中往往要对数据进行宰割,而后扩散到各个CPU下来解决,如果咱们应用的数据是数组构造则能够很轻易的进行宰割,然而如果是链表构造的数据或者Hash构造的数据则宰割起来很显著不如数组构造不便。

所以只有当Stream中元素过万甚至更大时,选用并行流能力带给你更显著的性能晋升。

最初,当你有一个并行流的时候,你也能够通过sequential() 将其不便的转换成串行流:

        Stream.of(1, 2, 3).parallel().sequential();

2.4 连贯Stream

如果你在两处结构了两个Stream,在应用的时候心愿组合在一起应用,能够应用concat():

        Stream<Integer> concat = Stream                .concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6));

如果是两种不同的泛型流进行组合,主动推断会主动的推断出两种类型雷同的父类:

        Stream<Integer> integerStream = Stream.of(1, 2, 3);        Stream<String> stringStream = Stream.of("1", "2", "3");        Stream<? extends Serializable> stream = Stream.concat(integerStream, stringStream);

3. Stream转换操作之无状态办法

无状态办法:即此办法的执行无需依赖后面办法执行的后果集。

在Stream中无状态的API咱们罕用的大略有以下三个:

  1. map()办法:此办法的参数是一个Function对象,它能够使你对汇合中的元素做自定义操作,并保留操作后的元素。
  2. filter()办法:此办法的参数是一个Predicate对象,Predicate的执行后果是一个Boolean类型,所以此办法只保留返回值为true的元素,正如其名咱们能够应用此办法做一些筛选操作。
  3. flatMap()办法:此办法和map()办法一样参数是一个Function对象,然而此Function的返回值要求是一个Stream,该办法能够将多个Stream中的元素聚合在一起进行返回。

先来看看一个map()办法的示例:

        Stream<Integer> integerStreamList = List.of(1, 2, 3).stream();        Stream<Integer> mapStream = integerStreamList.map(i -> i * 10);

咱们领有一个List,想要对其中的每个元素进行乘10 的操作,就能够采纳如上写法,其中的i是对List中元素的变量名, 前面的逻辑则是要对此元素进行的操作,以一种十分简洁明了的形式传入一段代码逻辑执行,这段代码最初会返回一个蕴含操作后果的新Stream。

这里为了更好的帮忙大家了解,我画了一个简图:


接下来是filter()办法示例:

        Stream<Integer> integerStreamList = List.of(10, 20, 30).stream();        Stream<Integer> filterStream = integerStreamList.filter(i -> i >= 20);

在这段代码中会执行i >= 20 这段逻辑,而后将返回值为true的后果保留在一个新的Stream中并返回。

这里我也有一个简略的图示:


flatMap() 办法的形容在上文我曾经形容过,然而有点过于形象,我在学习此办法中也是搜寻了很多示例才有了较好的了解。

依据官网文档的说法,此办法是为了进行一对多元素的平展操作:

        List<Order> orders = List.of(new Order(), new Order());        Stream<Item> itemStream = orders.stream()                .flatMap(order -> order.getItemList().stream());

这里我通过一个订单示例来阐明此办法,咱们的每个订单中都蕴含了一个商品List,如果我想要将两个订单中所有商品List组成一个新的商品List,就须要用到flatMap()办法。

在下面的代码示例中能够看到每个订单都返回了一个商品List的Stream,咱们在本例中只有两个订单,所以也就是最终会返回两个商品List的Stream,flatMap()办法的作用就是将这两个Stream中元素提取进去而后放到一个新的Stream中。

老规矩,放一个简略的图示来阐明:

图例中我应用青色代表Stream,在最终的输入中能够看到flatMap()将两个流变成了一个流进行输入,这在某些场景中十分有用,比方我下面的订单例子。


还有一个很不罕用的无状态办法peek()

    Stream<T> peek(Consumer<? super T> action);

peek办法承受一个Consumer对象做参数,这是一个无返回值的参数,咱们能够通过peek办法做些打印元素之类的操作:

        Stream<Integer> peekStream = integerStreamList.peek(i -> System.out.println(i));

然而如果你不太熟悉的话,不倡议应用,某些状况下它并不会失效,比方:

        List.of(1, 2, 3).stream()                .map(i -> i * 10)                .peek(System.out::println)                .count();

API文档下面也注明了此办法是用于Debug,通过我的教训,只有当Stream最终须要从新生产元素时,peek才会执行。

下面的例子中,count只须要返回元素个数,所以peek没有执行,如果换成collect办法就会执行。

或者如果Stream中存在过滤办法如filter办法和match相干办法,它也会执行。

3.1 根底类型Stream

上一节提到了三个Stream中最罕用的三个无状态办法,在Stream的无状态办法中还有几个和map()与flatMap()对应的办法,它们别离是:

  1. mapToInt
  2. mapToLong
  3. mapToDouble
  4. flatMapToInt
  5. flatMapToLong
  6. flatMapToDouble

这六个办法首先从办法名中就可以看进去,它们只是在map()或者flatMap()的根底上对返回值进行转换操作,按理说没必要单拎进去做成一个办法,实际上它们的关键在于返回值:

  1. mapToInt返回值为IntStream
  2. mapToLong返回值为LongStream
  3. mapToDouble返回值为DoubleStream
  4. flatMapToInt返回值为IntStream
  5. flatMapToLong返回值为LongStream
  6. flatMapToDouble返回值为DoubleStream

在JDK5中为了使Java更加的面向对象,引入了包装类的概念,八大根底数据类型都对应着一个包装类,这使你在应用根底类型时能够无感的进行主动拆箱/装箱,也就是主动应用包装类的转换方法。

比方,在最前文的示例中,我用了这样一个例子:

        Stream<Integer> integerStream = Stream.of(1, 2, 3);

我在创立Stream中应用了根本数据类型参数,其泛型则被主动包装成了Integer,然而咱们有时可能疏忽主动拆装箱也是有代价的,如果咱们想在应用Stream中疏忽这个代价则能够应用Stream中转为根底数据类型设计的Stream:

  1. IntStream:对应 根底数据类型中的int、short、char、boolean
  2. LongStream:对应根底数据类型中的long
  3. DoubleStream:对应根底数据类型中的double和float

在这些接口中都能够和上文的例子一样通过of办法结构Stream,且不会主动拆装箱。

所以上文中提到的那六个办法实际上就是将一般流转换成这种根底类型流,在咱们须要的时候能够领有更高的效率。

根底类型流在API方面领有Stream一样的API,所以在应用方面只有明确了Stream,根底类型流也都是一样的。

:IntStream、LongStream和DoubleStream都是接口,但并非继承自Stream接口。

3.2 无状态办法的循环合并

说完无状态的这几个办法咱们来看一个前文中的例子:

        List<Integer> list = List.of(1, 2, 3).stream()                .filter(i -> i > 2)                .filter(i -> i < 10)                .filter(i -> i % 2 == 0)                .collect(toList());

在这个例子中我用了三次filter办法,那么大家感觉Stream会循环三次进行过滤吗?

如果换掉其中一个filter为map,大家感觉会循环几次?

        List<Integer> list = List.of(1, 2, 3).stream()                .map(i -> i * 10)                .filter(i -> i < 10)                .filter(i -> i % 2 == 0)                .collect(toList());

从咱们的直觉来看,须要先应用map办法对所有元素做解决,而后再应用filter办法做过滤,所以须要执行三次循环。

但回顾无状态办法的定义,你能够发现其余这三个条件能够放在一个循环外面做,因为filter只依赖map的计算结果,而不用依赖map执行完后的后果集,所以只有保障先操作map再操作filter,它们就能够在一次循环内实现,这种优化形式被称为循环合并

所有的无状态办法都能够放在同一个循环内执行,它们也能够不便的应用并行流在多个CPU上执行。

4. Stream转换操作之有状态办法

后面说完了无状态办法,有状态办法就比较简单了,只看名字就能够晓得它的作用:

办法名办法后果
distinct()元素去重。
sorted()元素排序,重载的两个办法,须要的时候能够传入一个排序对象。
limit(long maxSize)传入一个数字,代表只取前X个元素。
skip(long n)传入一个数字,代表跳过X个元素,取前面的元素。
takeWhile(Predicate predicate)JDK9新增,传入一个断言参数当第一次断言为false时进行,返回后面断言为true的元素。
dropWhile(Predicate predicate)JDK9新增,传入一个断言参数当第一次断言为false时进行,删除后面断言为true的元素。

以上就是所有的有状态办法,它们的办法执行都必须依赖后面办法执行的后果集能力执行,比方排序办法就须要依赖后面办法的后果集能力进行排序。

同时limit办法和takeWhile是两个短路操作方法,这象征效率更高,因为可能外部循环还没有走完时就曾经选出了咱们想要的元素。

所以有状态的办法不像无状态办法那样能够在一个循环内执行,每个有状态办法都要经验一个独自的外部循环,所以编写代码时的程序会影响到程序的执行后果以及性能,心愿各位读者在开发过程中留神。

5. 总结

本文次要是对Stream做了一个概览,并讲述了Stream的两大特点:

  1. 不可变:不影响原汇合,每次调用都返回一个新的Stream。
  2. 提早执行:在遇到终结操作之前,Stream不会执行。

同时也将Stream的API分成了转换操作和终结操作两类,并解说了所有罕用的转换操作,下一章的次要内容将是终结操作。

在看Stream源码的过程中发现了一个乏味的事件,在ReferencePipeline类中(Stream的实现类),它的办法程序从上往下正好是:无状态办法 → 有状态办法 → 聚合办法。

好了,学完本篇后,我想大家对Stream的整体曾经很清晰了,同时对转换操作的API应该也曾经把握了,毕竟也不多,Java8还有很多弱小的个性,咱们下次接着聊~


同时,本文在写作过程中也参考了以下书籍:

  • 写给大忙人看的Java SE 8
  • Java 8 函数式编程
  • Java 8 实战

这三本书都十分好,第一本是Java核心技术的作者写的,如果你想全面的理解JDK8的降级能够看这本。

第二本能够说是一个小册子,只有一百多页很短,次要讲了一些函数式的思维。

如果你只能看一本,那么我这里举荐第三本,豆瓣评分高达9.2,内容和品质都当属上乘。