文档章节

学习 Java8 函数式编程 (三)

cnbo
 cnbo
发布于 2017/08/26 10:03
字数 2090
阅读 53
收藏 1

扯淡


在准备学 Java8 之前,我以为不会很难。所以,我就决定一边学 Java8,一边写博客。当我准备写这篇博客的时候,我发现两件都不容易。如果不是我亲身体验,我也没法知道一篇博客的背后,作者得付出多少时间和精力。这会让我在读每一篇博客时,保持对作者的敬畏之心。

本篇博客会简单介绍 Stream,以及认识是用 Stream 的 API。当我不知道这篇博客该怎么写的时候,我想试着从 Stream 的源码去解读。然后,我就傻逼似的,屁颠屁颠地去看 Stream 源码,结果可想而知,我抱着头顶上的一堆星星去床上思考人生了。Stream 的功能非常强大,给我们这些普通的程序员带来了极大的方便。这必然意味着,Java 的布道师们会在底层实现一堆逻辑非常复杂的代码。所以,学习一门新技术的时候,我们应该先知道这门技术是什么,可以干什么,然后熟练掌握,最后深究它背后的实现原理。

认识 Stream


第一眼看见 Stream 这个词的时候,会误认为和 Java 的 I/O 流有着不可描述的关系。其实,它们俩就像潘长江和曾志伟并不是兄弟一样,毫无关系。

其实和 Stream 有关系的是 Java 中的容器(我们经常使用的 List 和 Set 等集合)。在 Java8 之前,我们使用的是增强 for 语法糖和容器自身的 Iterator 来遍历数据。我们先来看下清单 1 中的代码,使用增强 for 和 Iterator 来遍历集合。

清单 1

List<String> strList = 
    Arrays.asList("a", "b", "c", "d");

//使用增强 for 遍历集合
for (String str : strList) {
    System.out.println(str);
}

//使用 Iterator 遍历集合
Iterator iterator = strList.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    System.out.println(str);
}

清单 1 中的代码使用了增强 for 语法糖和 Iterator 来遍历一个集合。其实使用增强 for 和 Iterator 是一回事。因为增强 for 是 Java 为简化我们使用 Iterator 而提供的一个语法糖,它背后的实现依然是 Iterator。大家可能会发现,无论我们是使用增强 for 还是 Iteraotr 遍历集合,都需要将集合中的元素一个个的取出来。这种将元素从集合中取出来的迭代方式叫外部迭代

现在,我们来看下清单 2 中的代码,使用 Java8 中的 Stream 来遍历集合。

清单 2

List<String> strList = 
    Arrays.asList("a", "b", "c",  "d");
strList.stream()
    .forEach(str -> System.out.println(str));

当大家读完清单 2 中的代码后,是不是似乎有点知道 Stream 是干啥的了。通过请单 2 中的代码,我们只能看出 Stream 有遍历的集合功能,而且写法更加简单,代码的可读性也增强了。使用 Stream 遍历集合的方式叫内部迭代

Java8 的布道师们利用了面向对象的思想,对容器的存储数据和遍历数据进行了解耦。从 Java8 开始,在绝大多数情况下,我们都会使用 Stream 来遍历集合,容器只用来存储数据,无需关心遍历。

按我的理解,Stream 就是一种新的迭代方式,它以一种更加简单的方式对数据进行处理,让代码简洁易懂。在接下来的内容中,大家会发现使用 Stream 处理数据的代码,我们能够清晰读出代码的意图,而且代码中没有多余的临时变量和命令式代码。同时,使用 Stream 操作数据,并不会改变数据源(可以是容器,可以是数组,也可以是其他类型的数据源)。在多核时代,我们可以使用 Stream 写出高效以及线程安全的代码。但这不意味着我们需要写线程相关的代码,因为 Stream 在底层已经帮我们实现了。

在接下来使用 Stream 的过程中,我们只需要在 Stream 的每个函数中传入一个函数,传入的函数只是用来告诉 Stream 需要做什么,具体该如何做,并不需要我们关心。

使用 Stream API


准备三个类

在使用 Stream API 之前,我们先准备三个类 Artist (创作音乐的个人或团队),Track (专辑中的一些曲目),Album (专辑,由若干曲目组成)。接下来的内容,都会围绕这三个类展开。这三个类的具体定义如清单 3, 4, 5 所示。

清单 3

public class Artist {
    
    //艺术家的名字(例如 “纵贯线乐队”)
    private String name;
    
    //乐队成员(例如 “周华健"),该字段可为空
    private String members;
    
    //乐队来自哪里(例如 “台湾”)
    private String origin;
    
    //省略 set 和 get 方法
             
}

清单 4

public class Track {
    //曲目名称
    private String name;
    
    //省略 set 和 get 方法

}

清单 5

public class Album {
    
    //专辑名
    private String name;
    
    //专辑上所有曲目的列表
    private List<Track> tracks;
    
    //参与创作本专辑的艺术家列表
    private List<Artist> musicians;
    
    //省略 set 和 get 方法
        
}

案例描述

问题是,找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有乐队,其中乐队名以 The 开头。

该问题可分解为如下几个步骤:

  • 找出专辑上的所有表演者
  • 分辨出哪些表演者是乐队
  • 找出每个乐队的国籍
  • 将找出的国籍放入一个集合

使用命令式代码实现

我们先是使用命令式风格的代码实现上述案例,代码如清单 6 所示。

清单 6

//将专辑中的艺术家为乐队的单独放入一个集合 bankList
List<Artist> artistList = album.getMusicians();
List<Artist> bankList = new ArrayList<>();
for (Artist artist : artistList) {
    if (artist.getName().startsWith("The")) {
        bankList.add(artist);
    }
}
        
//找出bankList中每个乐队的国籍,并将国籍放入originList
List<String> originList = new ArrayList<>();
for (Artist artist : bankList) {
    originList.add(artist.getOrigin());
}            

清单 6 的代码存在如下问题:

  • 存在多余的临时变量
  • 样板式代码掩盖了关键代码,代码的可读性很低

使用 Stream API 实现

我们现在使用 Stream API 来重写清单 6 中的代码,如清单 7 所示。

清单 7

Set<String> originList =
    album.getMusicians()
    .stream()
    .filter(artist -> artist.getName().startsWith("The"))
    .map(artist -> artist.getOrigin())
    .collect(toSet());

当大家看到清单 7 的代码时候,有没有很惊喜?反正我是为之跳跃。我先来解读下这段代码。其实,当大家熟悉 Stream 的 API 后,一眼就能看出这段代码的意图,它简直就是将问题的每一个小步骤描述了一遍,没有一点拖泥带水。首先通过 getMusicians 获取 album (专辑) 中的艺术家列表;然后使用艺术家列表构建一个 Stream,这里要说的是,所有 Collection 的子类都可以使用 stream 方法来构建一个 Stream,因为 Java8 允许接口中有 default 方法;接着调用 Stream 的 filter 方法,并告诉它筛选出 Stream 中 艺术家名字以 The 开头的数据,将筛选出的数据组织成一个新的 Stream 返回;紧接着,调用 Stream 的 map 方法,将 Stream 中的艺术家映射为艺术家的国籍,返回新的 Stream;最后,使用 Stream 的 collect 方法生成一个 Set 集合。

经过清单6 中的代码与清单 7 中的代码进行比较,我想大家会一致认同,Stream 简直太好用了,写出来的代码简洁易懂。

结束语


本篇博客只是简单介绍了 Stream 和 使用 Stream 解决问题的具体案例,在后续博客中,我会更加细致地介绍 Stream。

彩蛋


我今天要给大家介绍的是,我的学长勇哥,这个是他在开源中国的地址 https://my.oschina.net/silence88。勇哥是个完美的 Java 程序员,人帅,会做菜,弹的一手好吉他(他就是因为吉他与我嫂子相识的),会打篮球,最牛逼的还是写代码厉害。勇哥的博客值得一读,你们会看到一个屌丝程序员是如何打怪升级的。勇哥是在大四的第一个学期开始自学 Java 的,他现在顺丰的丰巢就职。

感谢大家的阅读~

© 著作权归作者所有

共有 人打赏支持
cnbo
粉丝 5
博文 9
码字总数 15846
作品 0
深圳
程序员
Java 8 新语法习惯 (提倡使用有帮助的编码)

表达能力强是函数式编程的优势之一,但是这对于我们的代码意味着什么呢?在这部分内容中,我们将比较命令式和函数式代码的示例,判断这两种的表达能力和简洁性的能力。我们还能够了解到这些品...

晁东洋
01/08
0
0
Java8解决了什么?

在学习面向对象时,许多人都会用Java来举例子,但是其实Java并非纯正的面向对象语言,最明显的就是:int,double等基本类型不是对象。 自从java8出来过后,引入了流,函数式编程,就更不是在向...

MageekChiu
2017/08/02
0
0
Java8新特性:函数式接口@FunctionalInterface使用说明

Java8新特性:接口静态方法与默认方法 Java8新特性:函数式接口@FunctionalInterface使用说明 Java8新特性:方法引用 Java8新特性:函数式编程 我们常用的一些接口Callable,Runnable,Comparato...

键走偏锋
08/05
0
0
java8 Lambda表达式的新手上车指南(1)--基础语法和函数式接口

背景   java9的一再推迟发布,似乎让我们恍然想起离发布java8已经过去了三年之久,java8应该算的上java语言在历代版本中变化最大的一个版本了,最大的新特性应该算得上是增加了lambda表达式,...

冬至饮雪
2017/04/27
0
0
双11Java程序员书单推荐

Java 《Java核心技术卷I》 《Java核心技术卷II》 《Java编程思想》 《Java并发编程实战》 《Effective Java》 《Java8实战》 《Java8函数式编程思维》 《深入理解Java虚拟机》 《Java性能权威...

黄步欢
2017/11/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

day92-20180918-英语流利阅读-待学习

健身最大的敌人不是懒惰,而是逞强 Daniel 2018-09-19 1.今日导读 还记得 2008 年北京奥运会运动员刘翔的退赛风波吗?那天几乎所有中国人都将视线聚焦在了鸟巢体育馆 110 米栏的项目上,迫不...

飞鱼说编程
16分钟前
1
0
70.shell的函数 数组 告警系统需求分析

20.16/20.17 shell中的函数 20.18 shell中的数组 20.19 告警系统需求分析 20.16/20.17 shell中的函数: ~1. 函数就是把一段代码整理到了一个小单元中,并给这个小单元起一个名字,当用到这段...

王鑫linux
今天
3
0
分布式框架spring-session实现session一致性使用问题

前言:项目中使用到spring-session来缓存用户信息,保证服务之间session一致性,但是获取session信息为什么不能再服务层获取? 一、spring-session实现session一致性方式 用户每一次请求都会...

WALK_MAN
今天
6
0
C++ yield()与sleep_for()

C++11 标准库提供了yield()和sleep_for()两个方法。 (1)std::this_thread::yield(): 线程调用该方法时,主动让出CPU,并且不参与CPU的本次调度,从而让其他线程有机会运行。在后续的调度周...

yepanl
今天
4
0
Java并发编程实战(chapter_3)(线程池ThreadPoolExecutor源码分析)

这个系列一直没再写,很多原因,中间经历了换工作,熟悉项目,熟悉新团队等等一系列的事情。并发课题对于Java来说是一个又重要又难的一大块,除非气定神闲、精力满满,否则我本身是不敢随便写...

心中的理想乡
今天
55
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部