《Java8 Stream介绍》

来源:互联网 发布:南山软件产业基地一栋 编辑:程序博客网 时间:2024/03/28 17:45

《Java8 Stream介绍》

1、为什么需要 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

2、小例子

在详细介绍之前,看一个例子(例子来源于:http://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/)

假设有这样一个需求:如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合

我们一般的写法如下:

    List<Transaction> groceryTransactions = new Arraylist<>();    //第一步:在所有的交易集合中,得到所有type为grocery的所有交易    for(Transaction t: transactions){     if(t.getType() == Transaction.GROCERY){        groceryTransactions.add(t);     }    }    //第二步:按照交易值降序排序    Collections.sort(groceryTransactions, new Comparator(){         public int compare(Transaction t1, Transaction t2){         return t2.getValue().compareTo(t1.getValue());     }    });    //第三步:得到最后的结果:ID集合    List<Integer> transactionIds = new ArrayList<>();    for(Transaction t: groceryTransactions){     transactionsIds.add(t.getId());    }

如果对命令式编程和声明式编程者两个概念比较熟悉的朋友,可以很明显的看出,以上代码就是明显的“命令式编程”。

这里说下命令式编程和声明式编程的概念。

命令式编程:简单来说就是手把手的告诉机器怎么来做这个事情。即命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:简单来说就是告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

拉回到我们上面的应用场景,如果我们使用Stream来完成,写法如下:

    List<Integer> transactionsIds = transactions.parallelStream().     filter(t -> t.getType() == Transaction.GROCERY).     sorted(comparing(Transaction::getValue).reversed()).     map(Transaction::getId).     collect(toList());

这是一段声明式编程的代码段,相比命令式,要简洁和清晰的多哈。在详细讲解Stream知识之前,稍微讲解一下上面代码的意思,不明白也没有关系,等把后面看完Stream知识之后再回头看这段代码也就相当容易了:

1、transactions.parallelStream():转化为并行Stream

2、filter(t -> t.getType() == Transaction.GROCERY):过滤只包含所有type为GROCERY的transaction。

3、sorted(comparing(Transaction::getValue).reversed()):根据指定的条件排序

4、map(Transaction::getId):得到每个transaction的ID

5、collect(toList()) 转化为List。

3、Stream

3.1、什么是Stream

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,具体这些操作如何应用到每个元素上,就给Stream就好了,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

3.2、Stream(流)的构成

当我们使用一个流的时候,通常包括三个基本步骤:

1)创建Stream;

2)转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换);

3)对Stream进行聚合(Reduce)操作,获取想要的结果;

第二步中每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列(即链式操作),变成一个管道。

看一个简单的例子:

    import java.util.List;    import com.google.common.collect.Lists;    public class StreamDemo01 {        public static void main(String[] args) {            List<String> names=Lists.newArrayList("wu",null,"hao","heihei");            int count=(int) names.stream().filter(name->name!=null).count();            System.out.println(count);        }    }

下面我们来分析上面例子中的这行代码:names.stream().filter(name->name!=null).count();

1、names.stream():创建stream

2、filter(name->name!=null):转换Stream,功能为过滤掉name为null的元素。

3、count():对stream进行聚合操作。

3.2 创建Stream的方式

有多种方式创建 Stream,最常用的创建Stream的有两张方式:

1、通过Stream接口的静态工厂方法,一般都是采用Stream.of()方法

注意:Java8里接口可以带静态方法,这里强调一下:接口中的静态方法必须要有实现;

在Stream接口中提供了以下几种静态方法来创建Stream。源码如下:

    //创建一个空的串行流.    public static<T> Stream<T> empty() {        return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);    }    //创建一个包含一个元素的串行流    public static<T> Stream<T> of(T t) {        return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);    }    //创建一个包含指定元素集的串行流    @SafeVarargs    @SuppressWarnings("varargs") // Creating a stream from an array is safe    public static<T> Stream<T> of(T... values) {        return Arrays.stream(values);    }    //创建一个无限的顺序流    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {        Objects.requireNonNull(f);        final Iterator<T> iterator = new Iterator<T>() {            @SuppressWarnings("unchecked")            T t = (T) Streams.NONE;            @Override            public boolean hasNext() {                return true;            }            @Override            public T next() {                return t = (t == Streams.NONE) ? seed : f.apply(t);            }        };        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(                iterator,                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);    }    //生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)    public static<T> Stream<T> generate(Supplier<T> s) {        Objects.requireNonNull(s);        return StreamSupport.stream(                new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);    }    //用两个Stream创建一个新的Stream。    public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {        Objects.requireNonNull(a);        Objects.requireNonNull(b);        @SuppressWarnings("unchecked")        Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(                (Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());        Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());        return stream.onClose(Streams.composedClose(a, b));    }

2、通过Collection接口的默认方法–stream(),把一个Collection对象转换成Stream

默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法,在以前这篇博文中有对接口中方法定义规则进行介绍:http://blog.csdn.net/u010412719/article/details/52536233

Collection接口有一个stream方法,所以其所有子类都都可以获取对应的Stream对象。

Collection.stream() :创建串行的Stream对象

Collection.parallelStream():创建并行的Stream对象

当然,数组也可以创建Stream:Arrays.stream(T array)

还有其它方式也可以创建Stream对象,这里不再介绍。

3.3 流的操作类型

流的操作类型分为两种:

1、Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。

其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered等相关的操作都是属于这一类的,这些操作的具体功能将在3.4节详细介绍。

2、Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator等操作都是属于这一类的。

还有一种操作被称为 short-circuiting

short-circuiting用以指:
对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

3.4 流的转换

转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream。

Stream接口中定义了几个常用的转换方法,下面我们挑选几个常用的转换方法来解释。

1、distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;

看一个distinct的例子

    public static void main(String[] args) {        List<String> names=Lists.newArrayList("wu","hao","heihei","hao");        List<String> distinctNames=names.stream().distinct().collect(Collectors.toList());        System.out.println(distinctNames);    }

输出:[wu, hao, heihei]

2、 filter: 对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;

看一个例子

    public static void main(String[] args) {        List<String> names=Lists.newArrayList("wu",null,"hao","heihei");        List<String> namesAfterFilter = names.stream().filter(name->name!=null).collect(Collectors.toList());        System.out.println(namesAfterFilter);    }

输出:[wu, hao, heihei]

3、map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;

看一个关于map使用的例子

    public static void main(String[] args) {        List<String> names=Lists.newArrayList("wu","hao","heihei","hao");        List<String> upperCaseNames=names.stream().map(name->name.toUpperCase()).collect(Collectors.toList());        System.out.println(upperCaseNames);    }

输出:[WU, HAO, HEIHEI, HAO]

4、flatMap:和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

5、peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;

6、limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;

7、skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

以上提供的一些转换Stream函数还是比较好理解的好。以上的Stream的转换操作就是3.3节中提到的Intermediate操作。

这里需要再强调一次的是:每次转换,原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列(即链式操作),变成一个管道。

3.5 汇聚(Reduce)Stream

在介绍汇聚操作之前,我们先看一下Java doc中对于其定义:

A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count().

翻译如下:汇聚操作(也称为折叠)接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

汇聚(Reduce)Stream也就是在3.3节中提到的Terminal操作。Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

汇聚操作在前面举例的过程中,就已经使用过了,例如下面这个例子中所使用的collect。

    public static void main(String[] args) {        List<String> names=Lists.newArrayList("wu","hao","heihei","hao");        List<String> upperCaseNames=names.stream().map(name->name.toUpperCase()).collect(Collectors.toList());        System.out.println(upperCaseNames);    }

下面这个例子中所使用的count,都是对Stream的聚合操作。

        public static void main(String[] args) {            List<String> names=Lists.newArrayList("wu",null,"hao","heihei");            int count=(int) names.stream().filter(name->name!=null).count();            System.out.println(count);        }

以上所使用的汇聚操作就对应的两种不同的汇聚操作。

1、可变汇聚:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;

可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。

2、其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

4、小结

总之,Stream 的特性可以归纳为:

1、Stream本身不是数据结构

2、它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。

3、它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。

4、所有 Stream 的操作必须以 lambda 表达式为参数

5、不支持索引访问

6、你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。

7、很容易生成数组或者 List

8、惰性化

9、很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。

10、Intermediate 操作永远是惰性化的。

11、并行能力;
当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

12、可以是无限的
集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

更多详细的介绍请参考本文所列出的参考资料。

参考资料

1、http://ifeve.com/stream/

2、http://blog.csdn.net/renfufei/article/details/24600507

3、http://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

0 0