2018-11-04

java学习笔记(五)

这篇简单的介绍流

集合是 Java 中使用最多的 API 。几乎每个 Java 应用都会制造和处理集合,但集合操作却算不上完美。为了让程序员活得更轻松,Java 语言设计者提供一种新的方式,那就是流。

什么是流

流是 Java API 的新成员,它允许你以声明式的方式处理集合(通过查询语句来表达,而不是临时编写一个实现)。你可以把它们看成遍历数据集合的高级迭代,而且流是可以透明的并行处理,你无需写任何多线程代码。下面两段代码都是用来返回低热量的菜肴名称,并按卡路里排序,一个是 Java7 写的,另一个是 Java8 的流写的。

Java7:

List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : menu) {
    if (d.getCalories() < 400) {
        lowCaloric.add(d);
    }
}

Collections.sort(lowCaloricDishes. new Comparator(){
    public int compare(Dish d1, Dish d2) {
        return Integer.compare(d1.getCalories(), d2.getCalories());
    }
});

List<String> lowCaloricDishesName = new ArrayList<>();
for (Dish d : lowCaloricDishes) {
    lowCaloricDishesName.add(d.getName());
}

在这段代码中,你使用了一个”垃圾变量” lowCaloricDishes, 它的作用就是作为一次性的中间容器。

Java8:

List<String> lowCaloricDishesName = menu.stream()
                                    .filter(d -> d.getCalores() < 400)
                                    .sorted(comparing(Dish::getCalories))
                                    .map(Dish::getName)
                                    .collect(toList());

如果你想使用多核架构执行这段代码,你可以这样:

List<String> lowCaloricDishesName = menu.parallelStream()
                                    .filter(d -> d.getCalores() < 400)
                                    .sorted(comparing(Dish::getCalories))
                                    .map(Dish::getName)
                                    .collect(toList());

选择,你可以看出几个显而易见的好处:

  • 代码是声明式的,说明想要完成什么而不是说明如何实现一个操作。这种方法加上行为参数化很容易应对变化的需求。
  • 你可以把几个操作连接起来,来表达复杂的数据处理流水线,同时保留代码的清晰可读。

filter 、sorted 、map 和 collect 等操作是和线程模型无关的高层次构件,所以它们的内部实现可以是单线程,也可以是多核架构。

新的 Stream API 表达能力非常强,你在简单学习后可以写出如下代码:

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

这行代码的意思是,按照 map 里面的类别对菜肴进行分组。

简单的总结一下,Java8 的 Stream API 可以让你写出这样代码:

  • 声明性——更简单,更易读
  • 可复合——更灵活
  • 可并行——性能更好

流简介

要讨论流,就先谈谈集合,这是最容易上手的方式了。Java8 中的集合支持一个新的 stream 方法,它返回一个流。

那么,流到底是什呢? 简短的定义就是:“从支持数据处理操作的源生成的元素序列”让我们进一步剖析这个定义:

  • 元素序列——就象集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合是数据结构,它的主要目的是以特定的时间或空间复杂度存储和访问元素。但流的目的在于表达计算。
  • 源——流会使用提供数据的源,如集合、数组或输入输出资源。有序集合产生的元会保留其顺序,列表生成的流顺序和列表一致。
  • 数据处理操作——流的数据处理能支持类似于数据库的操作,以及函数式编程语言中的常用操作。如 filter 、map 、reduce 、find 、match、sort 等

此外还有两个特性:

  • 流水线——很多流操作本身都会返回一个流,这样多个操作就能连接起来,形成一个流水线。
  • 内部迭代——与使用迭代器的显示迭代不同,流的操作是在背后进行的。

让我们看一段能体现下面新概念的代码:

List<String> threeHighCaloricDishName = 
    menu.stream()           //从menu获得流  
        .filter(d -> d.getCalores() > 300)  //建立操作流水线:首先选出高热量的菜肴
        .map(Dish::getName)     //获取菜名
        .limit(3)              //只需按着前三个
        .collect(toList());     //将结果保留到另一个集合中
System.out.println(threeHighCaloricDishName);

简单的介绍上面的各种流的操作:

  • filter —— 接受 Lambda ,从流中排除某些元素。在本例中,通过传递 Lambda d -> d.getCalories() > 300,选择出热量超过 300 卡路里的菜肴。
  • map —— 接受一个 Lambda ,将元素转化为其他形式或提取信息。在本例中,通过传递方法引用 Dish::getName,相当于 Lambda d -> d.getName(),提取每一道菜名。
  • limit —— 截断流,时期元素不超过给定的数量。
  • collect —— 将流转化为其他形式。本例中,将流转化为一个列表。

流与集合

Java 现有的集合概念和新的流的概念都提供了接口,来配合表达元素类型和有序值的数据接口。所谓有序,就是按顺序去所用值,而不是随机取用。

粗略地说,流与集合的差异在于什么时候计算。集合是一个内存中的数据结构,它包含数据结构目前所有值--集合的每个元素都得先算出来才能添加到集合中。

相比之下,流则在概念上是固定的数据结构(你不能添加和删除元素),其他元素则是按需计算。

只能遍历一次

和迭代器类似,流只能遍历一次。遍历完之后,这个流就已经被消费了。你可以从数据源从新获取再次遍历,就像迭代器一样。例如:下面代码就会抛出异常。

List<String> title = Arrays.asList("Java8", "in", "action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
//这句话会报异常,应为流已被操作
s.forEach(System.out::println);

所以务必记住,留着能被消费一次。

外部迭代和内部迭代

使用 collection 接口需要用户自己去迭代(如 for-each),则成为外部迭代。相反,流使用内部迭代 —— 它帮你迭代。下面代码说明区别:

集合:用 for-each外部迭代

List<String> names = new ArrayList<>();
for (Dish d : menu) {
    names.add(d.getName());
}

集合:用背后的迭代器迭代

List<String> names = new ArrayLsit<>();
Iterator<Dish> iterator = menu.iterator();
while (iterator.hasNext()) {
    Dish d = iterator.next();
    names.add(d.getName());
}

流:内部迭代

List<String> names = menu.stream().map(Dish::getName()).collect(toList());

流操作

Stream 接口定义了 许多操作,基本可以分为两大类操作:

  • 例如:filter 、map 和 limit 等方法,可以形成流水线的中间操作。
  • 例如:collection 触发流水线执行关闭的终端操作。

中间操作

像 filter 和 map 等中间操作会返回另一个流,这让可以让多个操作连接在一起形成一个查询。除非执行一个终端操作,否则中间操作不会正真执行 —— 他们都是延时执行的。这是因为中间操作一般都可以合并起来,在终端操作中一起执行。

你可以修改代码,做一下尝试:

List<String> names = 
    menu.stream()
    .filter(d -> {
        System.out.println("filtering: " + d.getName());
        return d.getCalories() > 300;
    })
    .map(d -> {
        System.out.println("mappong: " + d.getName());
        return d.getName();
    })
    .limit(3);
    .collect(toList());
//打印结果菜名
System.out.println(names);

你会得到如下结果:

filtering pork
mapping pork
filtering beef
mapping beef
filtering chicken
mapping chicken
[pork, beef, chicken]

有些操作就是利用了流的这种延时性质。尽管 filter 和 map 是个两个独立的操作,但是它们被合并到同一次遍历中(这种技术叫循环合并)。

终端操作

终端操作会从流水线中生成结果,结果是一个非流值。比如List 、Integer,甚至是 void 。例如:下面这个 forEach 操作就返回一个 void 的终端操作。

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

使用流

总而言之,流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询。
  • 一个中间链操作,形成一条流水线。
  • 一个终端操作,执行流水线,并能生成结果 。

流的流水线背后的理念类似于构建器模式。在构建器模式中有一条调用连用来设置一套配置(流的中间链),接着调用 build 方法(流的终端操作)。

总结

  • 流是“从支持数据处理操作的源生成的一系列的元素”。
  • 流是内部迭代:迭代通过 filter 、map 、sorted等操作被抽象掉了。
  • 流的操作有两类:中间操作和终端操作。
  • filter 和 map 等中间操作会返回一个流,并可以连接在一起,形成一条流水线,但不会产生任何结果。
  • forEach 和 count 等终端操作会返回一个非流值,并处理流水线以返回结果。
  • 流中的元素是按需计算的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,490评论 0 4
  •   集合是Java中使用最多的API。要是没有集合,还能做什么呢?几乎每个java应用程序都会制造处理集合。集合对...
    琼珶和予阅读 475评论 0 0
  • 流是什么 它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。可以把它们看成遍历数据集...
    邪恶的Sheldon阅读 609评论 0 0
  • 从书名上,很难知道这是关于什么内容的书,这是一本纪实文学,关于苏联解体。 这是我读过的第二本纪实文学,第...
    空地阅读 314评论 0 0
  • 咖啡的利与弊: 1.咖啡含有一定的营养成分。咖啡的烟碱酸含有维他命B,烘焙后的咖啡豆含量更高。并且有游离脂肪酸、咖...
    Una520阅读 498评论 0 4