介绍
JDK 8 不止新增了 Lambda 表达式,还有 Stream
流 ,程序员通过 Stream
流来简化对数据的处理。其本质就是计算。
可以这么理解:流就是数据通道,用于操作数据源所生成的元素序列。
我们来熟悉一下 Stream
流:
1 | java复制代码public class StringSorting { |
输出结果:
1 | shell复制代码C C# C++ Clojure Groovy Java JavaScript Jython Kotlin Python Ruby SQL Scala Shell |
由上面的例子,我们可以得知使用 Stream
的步骤:
- 创建流。例如上面的
Stream.of()
- 对流的中间操作。例如上面的
sorted()
,在流中对数据进行排序。 - 最终操作。例如上面的
forEach()
,将流通过该方法进行打印。
流创建
流的创建主要有以下几种方式:
以下几种方式可以创建 Stream
流:
Arrays
数组工具类提供的stream()
静态方法。Stream
类中of()
、generate()
、iterate()
和empty()
静态方法。Stream
类中的builder()
方法。Collection
集合提供的stream()
方法与parallelStream()
方法。
还有其它的一些产生流的方法就不一一列举了。下面讲解一下上述创建 Stream
流的方法
Arrays.stream() 静态方法
以下是 Arrays
关于产生流的方法 stream()
及重载方法:
1 | java复制代码public class Arrays { |
由以上方法可知,通过重载形式,可以满足我们调用的基本类型与泛型的数组转化为 Stream
流。
下面是使用 Arrays.stream()
来将数组转换为流的例子:
1 | java复制代码public class ArraysStream { |
Stream 的静态方法
Stream
流中共有 of()
、generate()
和 iterate()
三个静态方法。
我们先看一下 Stream.of()
静态方法的源码:
1 | java复制代码public interface Stream<T> extends BaseStream<T, Stream<T>> { |
由以上源码可知,使用 Stream.of()
静态方法,是将可变元素传递到 Arrays
数组工具类的静态方法 stream()
去转换为流,其实质还是调用 Arrays.stream()
方法。
下面我们来看一下,使用 Stream.of()
来将可变参数数组转化为流的例子:
1 | java复制代码public class StreamOf { |
Stream.of()
静态方法的源码和例子如上所述。·
Stream
还有产生无限元素的静态方法用于产生流,它就是generate()
静态方法:
1 | java复制代码public interface Stream<T> extends BaseStream<T, Stream<T>> { |
使用 generate()
方法创建的流,长度是无限的,它的参数是 Supplier
函数接口。
1 | java复制代码public class StreamGenerate { |
输出结果:
1 | shell复制代码0.792192 0.625073 0.983115 0.372405 0.294238 0.790635 0.688823 0.238186 0.096708 0.434963 |
我们在看另一个创建无限流的静态方法 Stream.iterate()
:
1 | java复制代码public interface Stream<T> extends BaseStream<T, Stream<T>> { |
上述两个静态方法都是 iterate
用来创建无限流的,与 generate
方法不同的是,它是通过函数f
迭代给指定的种子而产生无限连续有序的Stream
。
1 | java复制代码public class StreamIterate { |
输出结果:
1 | shell复制代码6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 |
Stream
类中还有一个创建不含任何元素的 Stream
流:
1 | java复制代码public interface Stream<T> extends BaseStream<T, Stream<T>> { |
创建空流的例子如下:
1 | java复制代码public class StreamEmpty { |
Stream的构造器模式
Stream
类还可以使用构建器 builder()
方法创建流。
1 | java复制代码public class StreamBuilder { |
输出结果:
1 | shell复制代码Jan Feb Mar Apr May Jun |
Collection系列
Java 8 中,扩展了 Collection
接口,添加了两个默认方法,来创建流:
1 | java复制代码public interface Collection<E> extends Iterable<E> { |
上面的两个默认方法,stream()
是转化为顺序流,paralletlStream()
转换为并行流。
下面我们看一下两个默认方法的使用例子:
1 | java复制代码public class ListStream { |
输出结果:
1 | shell复制代码Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec |
但将 List
对象的 stream()
方法变换为 parallelStream()
方法后,
输出结果:
1 | java复制代码Aug Jul Jun Feb Dec Oct Nov Apr Mar May Jan Sep |
从上面我们可以看出,stream()
与 parallelStream()
的区别:
stream()
是转化为顺序流,它使用主线程,是单线程的;parallelStream()
是转化为并行流,它是多个线程同时运行的。因此,stream()
是按顺序输出的,而parallelStream()
不是。
那我们如何将 Map
关系映射表转化为流呢?
看下面的例子:
1 | java复制代码public class MapStream { |
输出结果:
1 | shell复制代码小孙=20 小李=29 小钱=29 小赵=18 |
从上面的例子就可得知,我们得到 Map
的 Entity
集合,Key
的集合和Value
的集合,在将它们转化为流去处理。
中间操作
中间操作就是对上述创建的流进行处理,处理完返回的还是流,以便于其它操作。
我们来看一下都有什么中间操作:peek()
、sorted()
、unordered()
、distinct()
、filter()
、map()
、flatMap()
、limit()
、skip()
等。下面我们来详细介绍这些操作。
peek
peek
操作的方法如下:
1 | java复制代码Stream<T> peek(Consumer<? super T> action); |
主要目的是接收一个 Consumer
函数提供的逻辑去对流中的元素进行操作。
peek()
操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例:
1 | java复制代码public class Peeking { |
输出结果:
1 | shell复制代码Hello |
再看下一个例子:
1 | java复制代码public class Peeking { |
输出结果:
1 | shell复制代码[HELLO, HELLO WORLD!, I'M HELLO] |
可以看出使用map()
操作对元素进行了转换,而使用 peek()
并没有对元素进行转换。
因此,peek()
一般用于不想改变流中元素本身的类型或者只想操作元素的内部状态时;而 map()
则用于改变流中元素本身类型,即从元素中派生出另一种类型的操作。
peek()
适用于对 Stream<T>
中 泛型的某些属性进行批处理的时候使用。
sorted
sorted
操作的方法如下:
1 | java复制代码Stream<T> sorted(); |
sorted()
可以使用无参方法,也可以传入 Comparator
参数。代码示例:
1 | java复制代码public class SortedComparator { |
输出结果:
1 | shell复制代码you what to the that sir leads in district And |
sorted()
预设了一些默认的比较器。这里我们使用的是反转 “自然排序”。当然你也可以把 Lambda 函数作为参数传递给 sorted()
。
distinct
distinct
的方法如下:
1 | java复制代码Stream<T> distinct(); |
它的作用主要是去重:
1 | java复制代码public class DistinctOperator { |
输出结果:
1 | shell复制代码45 31 21 98 55 982 54 |
filter
filter
的方法如下:
1 | java复制代码Stream<T> filter(Predicate<? super T> predicate); |
该方法是过滤操作,把不想要的数据过滤掉,留下想要的的数据。会根据传入的 Predicate
函数,将不符合的数据过滤掉,留下符合 Predicate
函数的数据。
我们看下面的例子来熟悉一下 filter()
方法的使用:
1 | java复制代码class Month { |
输出结果:
1 | shell复制代码Month{name='Jan', id=1} |
从结果中可以看到,凡是首字母与 J
相等的对象都被打印出来了。
map
map
的方法如下:
1 | java复制代码<R> Stream<R> map(Function<? super T, ? extends R> mapper); |
它接收一个 Function
函数,作用是将一个类型的对象转换为另一个类型的对象。
下面我们编写一个例子来看一下它的操作方式:
1 | java复制代码class Student { |
输出结果:
1 | shell复制代码小赵 小钱 小孙 小李 小周 小吴 小郑 小王 |
从上面的例子可知,第一个输出是从 Student
对象数组中将所有学生的姓名都获取出来并打印,第二个是打印年龄。
从第二个可以看出,源码已经封装好了转换为基本类型的 mapToInt
方法,返回的结果为 IntStream
,其它所有关于 map
操作的源码如下:
1 | java复制代码// 根据 `ToIntFunction` 函数,获取 `IntStream` 流 |
flatMap
flatMap
的方法如下:
1 | java复制代码<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); |
该方法接收一个 Function
函数作为参数,将流中的每个值都转换成另一个流,然后把所有流连接成一个流,是扁平化操作。
例子:
1 | java复制代码class Employee { |
输出结果:
1 | shell复制代码Employee{id=1, name='小赵', salary=1500.0} |
从结果上看,flatMap()
将两个数组转换成流并合并在一起。
flatMap()
也有封装好的具体类型的方法,源码如下:
1 | java复制代码IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper); |
limit
limit
的方法如下:
1 | java复制代码Stream<T> limit(long maxSize); |
它的作用是根据 maxSize
参数的大小,截取流,使其元素个数不超过 maxSize
。
我们给一个例子来熟悉 limit()
的使用:
1 | java复制代码public class LimitOperator { |
输出结果:
1 | shell复制代码Jan Feb Mar Apr May |
skip
skip
的方法如下:
1 | java复制代码Stream<T> skip(long n); |
跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit
互补。
1 | java复制代码public class SkipOperator { |
输出结果:
1 | java复制代码Nov Dec |
终端操作
终端操作就是为了结束流的中间操作,并返回结果。至此无法再继续往后传递流。
终端操作都有哪些操作呢?我们简单来看下:forEach()
、forEachOrdered()
、toArray()
、collect()
、reduce()
、min()
、max()
、count()
、anyMatch()
、allMatch()
、noneMatch()
、findFirst()
、findAny()
等。下面详细介绍一下这些终端操作的使用。
forEach
forEach
的方法如下:
1 | java复制代码void forEach(Consumer<? super T> action); |
主要是进行编译操作,我们传递进去一个 Consumer
函数,Stream
在内部进行迭代。
1 | java复制代码public class ForEachOperator { |
forEachOrdered
forEachOrdered
的方法如下:
1 | java复制代码void forEachOrdered(Consumer<? super T> action); |
它的作用与 forEach()
一样,但是在并行流上的区别较大
1 | java复制代码public class ForEachOrderedOperator { |
输出结果:
1 | java复制代码Aug May Dec Oct Jun Feb Apr Nov Mar Jan Jul Sep |
从输出结果可知,使用并行流进行操作时,forEachOrdered()
会严格按照顺序进行输出,而forEach()
不会。
toArray
toArray
的方法如下:
1 | java复制代码Object[] toArray(); |
toArray
有两个方法,一个无参,一个传入
我们给出一个例子,来熟悉使用 toArray()
方法:
1 | java复制代码public class ToArrayOperator { |
输出结果:
1 | shell复制代码[Jan, Jun, Jul] |
从结果上看,我们知道 toArray()
方法就是将流转换为数组。
collect
collect
的方法如下:
1 | java复制代码<R, A> R collect(Collector<? super T, A, R> collector); |
collect
是收集操作。通过流将其转换为其它常用的数据结构并收集,比如转换成 List
、Map
等操作。
从上述源码中,看出 collect
方法一共有两个方法。
第一个方法从接收的参数可以看出,collect
方法是通过 java.util.stream.Collector
类来收集流元素到结果集中的。
1 | java复制代码public class CollectOperator { |
输出结果:
1 | shell复制代码[Jan, Jun, Jul, Jun, Jun, Jan] |
从结果中看到,我们通过 filter
过滤掉首字母不是 J
的字符串,并通过 collect
转换为 List
收集起来,但是收集的集合中有重复的元素,因此,我们将其转换成 Set
在收集。
1 | java复制代码public class CollectOperator { |
输出结果:
1 | shell复制代码[Jul, Jun, Jan] |
输出的结果不是有序的,为了保证元素的有序,我们将元素存储在 TreeSet
中。Collectors
中没有 toTreeSet()
,因此我们通过 Collectors.toCollection(Supplier<C> collectionFactory)
来构建我们需要的集合类型。
1 | java复制代码Stream.of("Jan", "Feb", "Mar", "Apr", "May", |
输出结果:
1 | shell复制代码[Jan, Jul, Jun] |
collect
也可以生成 Map
。代码如下:
1 | java复制代码class Pair { |
输出结果:
1 | shell复制代码{a=97, c=99, y=121, z=122, o=111} |
对于第二个方法,参数 Supplier
是一个生成目标类型实例的方法,代表着存储类型的对象;BiConsumer<R, ? super T> accumulator
参数是将操作目标数据填充到 Supplier
生成的目标类型的实例中去的方法,代表着如何将元素添加到容器中;而 BiConsumer<R, R> combiner
是将多个 Supplier
生成的实例整合到一起的方法,代表着规约操作,将多个结果合并。
下面给出一个简单的例子,统计月份出现的频次:
1 | java复制代码public class CollectOperator { |
输出结果:
1 | shell复制代码{Apr=1, Aug=1, Dec=1, Feb=2, Jan=2, Jul=1, Jun=3, Mar=1, May=1, Nov=1, Oct=1, Sep=1} |
reduce
reduce
的方法如下:
1 | java复制代码T reduce(T identity, BinaryOperator<T> accumulator); |
reduce
是通过反复的对一个输入序列的元素进行某种组合操作(求和、最大值或将所有元素都放入一个列表),最终将其组合为一个单一的概要信息。
1 | java复制代码public class ReduceOperator { |
输出结果:
1 | shell复制代码Jan,Jun,Jul |
Stream
中关于 min
、max
、count
的源码如下:
1 | java复制代码Optional<T> min(Comparator<? super T> comparator); |
count
的作用是求流中元素个数。
min
的作用是根据传入的 Comparator
来求 “最小” 的元素。
max
的作用是根据传入的 Comparator
来求 “最小” 的元素。
它们的底层都是依赖 reduce
实现的。
现在看一下例子代码:
1 | java复制代码public class InfoReduceOperator { |
输出结果:
1 | shell复制代码计算首字母是`J`的字符串的个数是:3个 |
match
match
是匹配操作,它总共有三类方法:anyMatch
、allMatch
和 noneMatch
。
anyMatch
用于检查所有元素中至少有一个是匹配的。它的方法如下:
1 | java复制代码boolean anyMatch(Predicate<? super T> predicate); |
allMatch
用于检查所有元素是否都匹配。方法如下:
1 | java复制代码boolean allMatch(Predicate<? super T> predicate); |
noneMatch
用于检查所有元素都没有匹配的。方法如下:
1 | java复制代码boolean noneMatch(Predicate<? super T> predicate); |
下面看一下关于上面方法的使用例子:
1 | java复制代码public class MatchOperator { |
输出结果:
1 | shell复制代码判断months数组中是否存在首字母为`A`:true |
find
find
是查找操作,它总共有三类方法:findFirst
和 findAny
。
findFirst
的作用是返回当前流中的第一个元素。方法如下:
1 | java复制代码Optional<T> findFirst(); |
findAny
的作用是返回当前流中的任意元素。方法如下:
1 | java复制代码Optional<T> findAny(); |
我们来一下它们的使用方法:
1 | java复制代码public class FindOperator { |
输出结果:
1 | shell复制代码获取流中第一个元素:Jan |
findFirst()
无论流是否是并行化,总是选择流中的第一个元素。
对于非并行流,findAny()
会选择流中的第一个元素。但当我们使用 parallel()
来并行化后,findAny()
才实现了选择任意元素。
如果必须选择流中最后一个元素,那就使用 reduce()
。
1 | java复制代码String last = Stream.of(months).parallel() |
reduce()
的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。
小结
使用 Stream
流对于Java是一个极大的提升,使用它编写代码看起来较为简洁优雅,并且Stream
流的并行化极大的发挥了现在多核时代的优势。
公众号 「海人为记」,期待你的关注!
回复「资源」即可获得免费学习资源!
本文转载自: 掘金