介绍
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流的并行化极大的发挥了现在多核时代的优势。
公众号 「海人为记」,期待你的关注!
回复「资源」即可获得免费学习资源!
本文转载自: 掘金