Java 8 | Stream

在学习 Stream 之前,我们先来了解一下,内部迭代与外部迭代。

外部迭代

直到 Java 7, 容器框架还是依赖于外部迭代的。什么是外部迭代呢? 这么来说, Collection 通过实现 Iterable 接口,提供了一种枚举容器中元素的方法。通过使用 Iterator, 我们可以依次遍历容器中的元素。例如,如果我们想把所有字符串变为大写形式,我们可以这么写:

public class IterationExamples {
    public static void main(String[] args){
        List<String> alphabets = Arrays.asList(new String[]{"a","b","b","d"});
         
        for(String letter: alphabets){
            System.out.println(letter.toUpperCase());
        }
    }
}

或者这样写

public class IterationExamples {
    public static void main(String[] args){
        List<String> alphabets = Arrays.asList(new String[]{"a","b","b","d"});
         
        Iterator<String> iterator = alphabets.listIterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next().toUpperCase());
        }
    }
}

上面的两个代码片段用的都是外部迭代。外部迭代已经非常简单了,但是还有几个不足:

  1. Java 的 for-each 循环/迭代本质上是有序的,它必须按照容器指定的顺序处理元素。
  2. 它限制了 JVM控制流程的可能性,而正是这种可能性使得 JVM 可以通过重新排序、并行处理、短路、延迟处理来提供更好的性能。

内部迭代

在一些情况下,我们是希望 for-each 循环保证串行性和有序性的。但是这往往降低了性能。与外部迭代相对应的是内部迭代,内部迭代不再需要用户自己控制迭代,而是把它交给 JVM,用户只需提供要在元素上执行的代码即可。

与上述示例等价的内部迭代代码如下:

public class IterationExamples {
    public static void main(String[] args) {
        List<String> alphabets = Arrays.asList(new String[] { "a", "b", "b", "d" });
        alphabets.forEach(l -> System.out.println(l.toUpperCase()));
    }
}

外部迭代把“做什么”( 转换为大写 ) 与 “怎么做”( 循环/迭代 ) 混淆在了一起,内部迭代只需用户提供“做什么”,而把“怎么做”交给了JVM。这提供了一些潜在益处:

用户代码只需关注解决问题,无需关注如何解决的细节,从而变得更加清晰了。
内部迭代使得 JVM 利用短路、并行处理和乱序执行来提升性能成为可能(JVM 是否利用了这些来优化性能取决于 JVM 实现本身,但是有了内部迭代这些至少是可能的,而对于外部迭代来说则是不可能的)。

我们知道了内部迭代如何帮助我们专注于业务逻辑而无需关心迭代过程,它使得代码更加简洁和具有可读性。在这篇文章中,我们要探讨 Java 8 新引入的另一个概念 - 流(Stream) 。

流(Stream) 可以定义为从一个源读取而来的支持聚集操作的一系列元素。这里说的源指的是集合或者数组,她们可以为源提供数据。流(Stream) 中数据的顺序和源保持一致。聚集操作或者批量操作允许我们很容易对流(Stream) 中的元素进行常见操作。

流 vs 集合

相信大家都在优酷上看过在线视频,当你开始观看视频时,文件的一小部分会首先下载到你的电脑中然后开始播放。在开始播放视频之前你并不需要把整个视频下载下来。我将尝试把这个概念与流关联起来。

在基本概念层面,集合与流的区别在于其中的元素是如何产生的。集合是存在于内存中的数据结构,其中保存了所有的元素对象,这些元素对象在加入到集合中之前,必须已经构建好。流本质上不是数据结构,其中的元素对象是按需产生的。这带来了显著的好处,用户只能从流中取到他们真正需要的元素,这些元素也只在用户真正需要的时候被生产出来。这看起来是一种生产者-消费者的关系。

在 Java 中, java.util.stream.Stream 代表流,在流上可以执行各种流操作。流操作分为中间操作和末端操作。末端操作会返回某一类型的结果,而中间操作会返回流对象,所以你可以把多个中间操作串成一行。流基于源而生成,比如 ListSet 都可以作为源(Map 不行)。流操作可以串行执行,也可以并行执行。

基于上述观点,我们可以总结出来流的基本特征:

  • 不是数据结构
  • 为 lambda 表达式而设计
  • 不支持基于索引的访问方式
  • 可以很容易的被转换为集合
  • 支持延迟访问
  • 可以并行执行

各种构建流对象的方式

下面是最常见的从集合产生流对象的方式

使用 Stream.of()

public class StreamBuilders {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("a", "b", "c", "d", "e", "f", "g");
        stream.forEach(p -> System.out.println(p));
    }
}

使用 Collection.stream()

public class StreamBuilders {
    public static void main(String[] args) {
        List<String> strings = Arrays.asList("a", "b", "c", "d", "e", "f", "g");
        Stream<String> stream = strings.stream();
        stream.forEach(p -> System.out.println(p));
    }
}

使用 generate()

public class StreamBuilders {
    public static void main(String[] args) {
        Stream<Double> stream = Stream.generate(() -> {
            return Math.random();
        }).limit(5);
        stream.forEach(p -> System.out.println(p));
    }
}

使用 iterate()

public class StreamBuilders {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.iterate(10, i -> i + 10).limit(5);
        stream.forEach(i -> System.out.println(i));
    }
}

使用 CharSequence.chars()

public class StreamBuilders {
    public static void main(String[] args) {
        IntStream stream = "ABCDEFG_abcdefg".chars();
        stream.forEach(p -> System.out.println(p));
    }
}

还有一些其它的方式也可以产生流对象,比如使用 java.util.stream.Stream.Builder<T>或者使用中间操作。

转换流为集合

其实不应该说是转换,因为这里并没有发生流对象转换为其它对象的过程,更应该说是把流中的元素放到其它数据结构中,比如集合或者数组。但是如果老是这么描述,有显得太过罗嗦,所以大家务必理解下面我提到的转换。

使用 stream.collect(Collectors.toList()) 把 Stream 转换为 List

public class StreamBuilders {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.stream();
        List<Integer> oddNumbersList = stream.filter(i -> i % 2 != 0).collect(Collectors.toList());
        System.out.print(oddNumbersList);
    }
}

使用 stream.collect(Collectors.toSet()) 把 Stream 转换为 Set

public class StreamBuilders {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.stream();
        List<Integer> oddNumbersSet = stream.filter(i -> i % 2 != 0).collect(Collectors.toSet());
        System.out.print(oddNumbersSet);
    }
}

使用 stream.toArray(EntryType[]::new) 把 Stream 转换为数组

public class StreamBuilders {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.stream();
        Integer[] oddNumbersArray = stream.filter(i -> i % 2 != 0).toArray(Integer[]::new);
        System.out.print(oddNumbersArray);
    }
}

还有许多方法可以把 Stream 转换为 Map 或其它数据类型。你只需要去浏览下 java.util.stream.Collectors,把它们记住即可。

核心的流操作

你可以在流对象上执行非常多有用的函数。 我并不打算把他们都讲到,但是我计划把最最重要的那些讲一下,这些都是你必须第一时间掌握的。

在继续之前,我们先构建一个字符串集合,下面的例子都会基于这个集合进行操作。

List<String> provinceNames = Arrays.asList("湖南", "湖北", "河南", "河北", "广东", "广西", "北京", "南京");

这些核心的流操作方法,可以划分为两类:

中间操作

中间操作返回流对象,所以你可以把多个中间操作串成一行。

filter()

filter 方法接受一个断言对象,用来过滤流中的元素。

provinceNames.stream().filter((p) -> p.startsWith("湖")).forEach(p -> System.out.print(p + ", "));

// 输出 : 
// 湖南, 湖北, 

map()

这个中间操作使用给定的函数把流中的每个元素转换为另一个对象。下面的例子把每个字符串转化为另一个字符串(末尾多了一个人字)。但是你也可以把每个对象转换为其它类型的对象。

provinceNames.stream().map(p -> p + "人, ").forEach(p -> System.out.print(p));

// 输出 : 
// 湖南人, 湖北人, 河南人, 河北人, 广东人, 广西人, 北京人, 南京人, 

sorted()

sorted 会对流中的元素进行排序,默认按自然序进行排序,除非你指定一个自定义的比较器(Comparator) 。

provinceNames.stream().sorted().map(p -> p + "人, ").forEach(p -> System.out.print(p));

// 输出 : 
// 北京人, 南京人, 广东人, 广西人, 河北人, 河南人, 湖北人, 湖南人, 

需要注意的是 sorted 仅仅对流中的元素进行排序,而不会影响后面的集合,集合中的元素排序保持不变。

末端操作

末端操作返回某一类型的结果,而不是流对象。

forEach()

该方法帮助迭代流中的元素并在元素上执行一些操作。这些操作可以是 lambda 表达式或者方法引用。

provinceNames.stream().forEach(System.out::println);

collect()

collect() 方法用来从流中抽取元素然后保存到一个集合或者数组中。

List<String> provinceNamesOrdered = provinceNames.stream().sorted().collect(Collectors.toList());
System.out.print(provinceNamesOrdered);

// 输出 : 
// [北京, 南京, 广东, 广西, 河北, 河南, 湖北, 湖南]

match()

各种匹配操作用来检查流中的元素是否指定断言条件。所有的匹配操作都是末端操作,它们返回布尔结果。

boolean matchedResult = provinceNames.stream().anyMatch((s) -> s.startsWith("湖"));
System.out.println(matchedResult);

matchedResult = provinceNames.stream().allMatch((s) -> s.startsWith("湖"));
System.out.println(matchedResult);

matchedResult = provinceNames.stream().noneMatch((s) -> s.startsWith("湖"));
System.out.println(matchedResult);

// 输出 : 
// true
// false
// false

count()

count 末端操作返回流中元素个数。

long totalMatched = provinceNames.stream().count();
System.out.println(totalMatched);

// 输出 : 8

reduce()

该末端操作使用给定的函数对流中的元素进行归约。它是这样一个过程:每次迭代,将上一次的迭代结果(第一次时为 identity 的元素,如没有 identity 则为流中的第一个元素)与下一个元素一同执行一个二元函数。在 reduce 操作中,identity 是可选的,如果使用,则作为第一次迭代的第一个元素使用。归约的结果保存在 Optional中。

Optional<String> reduced = provinceNames.stream().reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);

// 输出 : 
// 湖南#湖北#河南#河北#广东#广西#北京#南京

短路操作

流操作通常会在流中满足某一断言的所有元素上进行操作,有时我们希望在迭代过程中遇到匹配的元素时就终止操作,在外部迭代中,你需要写 if-else 代码块,在内部迭代中,有现成的方法可以使用,下面是两个这样的方法的示例:

anyMatch()

该操作只要遇到满足断言条件的元素就会返回 true,在此之后就不再处理任何其它的元素了。

boolean matched = provinceNames.stream().anyMatch((s) -> s.startsWith("河"));
System.out.println(matched);

// 输出 : true

findFirst()

该操作会返回流中的第一个元素,然后就不再处理其它元素了。

String firstMatchedName = provinceNames.stream().filter((s) -> s.startsWith("广")).findFirst().get();
System.out.println(firstMatchedName);

// 输出 : 广东

并行处理
有了 Java 7 中的 Fork/Join 框架,我们有了高效的实现并行操作的机制,但是使用这个框架非常复杂,如果实现不当,那就会引入无数的令人费解的多线程 bug,甚至可能使得应用程序崩溃。有了内部迭代,我们一样可以实现并行操作。

要启用并行性,你只需创建一个并行流,而不是串行流。这会让你觉得非常惊讶,因为真是太简单了。在上面所有的流操作例子中,任何时候你希望在多核中并行执行你的任务,你只需要改调用 parallelStream() 方法而不是 stream()方法。

public class StreamBuilders {
   public static void main(String[] args) {
       List<Integer> list = new ArrayList<Integer>();
       for (int i = 1; i < 10; i++) {
           list.add(i);
       }
       // 在这里创建并行流
       Stream<Integer> stream = list.parallelStream();
       Integer[] evenNumbersArr = stream.filter(i -> i % 2 == 0).toArray(Integer[]::new);
       System.out.print(evenNumbersArr);
   }
}

关于 Java 8 中的 Stream, 今天介绍到这了,在后续的文章中会继续探讨和 Stream 相关的话题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容

  • 本文翻译自The Java 8 Stream API Tutorial 1. 简介   本教程志在细致入微、深入底...
    Yodes阅读 2,461评论 3 8
  • 本文采用实例驱动的方式,对JAVA8的stream API进行一个深入的介绍。虽然JAVA8中的stream AP...
    浮梁翁阅读 25,757评论 3 50
  • 转自:IBM-developerworks Stream是什么 看到这个Stream的第一眼,我相信你可能会想到J...
    MentallyL阅读 1,400评论 0 1
  • 在一个名为“震撼人心的史诗音乐”的歌单下,我看完了Walter Isaacson的《乔布斯传》的最后几章,结束了三...
    匿蟒阅读 2,979评论 2 22
  • 母亲节前,女儿说要送我一条钻石项链。我本来就有一条项链,所以叫女儿别买。 到了母亲节那天,女儿拿出一个精致的盒子,...
    文采乐阅读 212评论 3 7