Java中的函数式编程(六)流Stream基础
写在前面
如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦。
只有熟悉了stream,你才能说熟悉了Java 的函数式编程。
本文主要介绍Stream的基础概念和基本操作,让大家对Stream有一个初步的理解。
本文的示例代码可从gitee上获取:https://gitee.com/cnmemset/javafp
stream的概念
首先,看一个典型的stream例子:
public static void simpleStream() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
int letterCount = words.stream()
.filter(s -> s.length() > 3) // 过滤掉长度小于等于3的单词
.mapToInt(String::length) // 将每个单词映射为单词长度
.sum(); // 计算总长度 5(hello) + 5(world) + 4(love) = 14
// 输出为 14
System.out.println(letterCount);
}
在上述例子中,我们将字符串列表 words 作为stream的数据源,然后执行了 filter-map-reduce 的系列操作(sum方法属于 reduce 操作),后面会详细介绍map和reduce 操作。如果你有大数据的编程经验,会更容易理解map和reduce的含义。
stream的定义比较晦涩,大致可以理解为是一个支持串行或并行操作的数据元素序列。它具备以下几个特点:
- 首先,stream不是一种数据结构,它并不存储数据。stream是某个数据源之上的数据视图。数据源可以是一个数组,或者是一个Collection类,甚至还可以是I/O channel。它通过一个计算管道(a pipeline of computational operations),对数据源的数据进行filter-map-reduce的操作。
- 其次,stream天生支持函数式编程。函数式编程的一个重要特点就是不会修改变量的值(没有“副作用”)。而对stream的任何操作,都不会修改数据源中的数据。例如,对一个数据源为Collection的stream进行filter操作,只会生成一个新的stream对象,而不会真的删除底层数据源中的元素。
- 第三,stream的许多操作都是惰性求值的(laziness-seeking)。惰性求值是指该操作只是对stream的一个描述,并不会马上执行。这类惰性的操作在stream中被称为中间操作(intermediate operations)。
- 第四,stream呈现的数据可以是无限的。例如Stream.generate可以生成一个无限的流。我们可以通过 limit(n) 方法来将一个无限流转换为有限流,或者通过 findFirst() 方法终止一个无限流。
- 最后,stream中的元素只能被消费1次。和迭代器 Iterator 相似,当需要重复访问某个元素时,需要重新生成一个新的stream。
stream的操作可以分成两类,中间操作(intermediate operations)和终止操作(terminal operations)。一个stream管道(stream pipeline)是由一个数据源 + 0个或多个中间操作 + 1个终止操作组成的。
中间操作:
中间操作(intermediate operations)指的是将一个stream转换为另一个stream的操作,譬如filter和map操作。中间操作都是惰性的,它们的作用仅仅是描述了一个新的stream,不会马上被执行。
终止操作:
终止操作(terminal operations)则指的是那些会产生一个新值或副作用(side-effect)的操作,譬如count 和 forEach 操作。只有遇到终止操作时,之前定义的中间操作才会真正被执行。需要注意,当一个stream执行了一个终止操作后,它的状态会变成“已消费”,不能再被使用。
为了证实“中间操作都是惰性的”,我们设计了一个实验性的示例代码:
public static void intermediateOperations() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
System.out.println("start: " + System.currentTimeMillis());
Stream<String> interStream = words.stream()
.filter(s -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
return s.length() > 3;
});
IntStream intStream = interStream.mapToInt(s -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
return s.length();
});
// 因为 filter 和 map 操作都属于中间操作,并不会真正执行,
// 所以它们不受 Thread.sleep 的影响,耗时很短
System.out.println("after filter && map: " + System.currentTimeMillis());
int letterCount = intStream.sum();
// sum 属于终止操作,会执行之前定义的中间操作,
// Thread.sleep 被真正执行了,耗时为 5(filter) + 3(mapToInt) = 8秒
System.out.println("after sum: " + System.currentTimeMillis());
// 输出为 14
System.out.println(letterCount);
}
上述代码的输出类似:
start: 1633438922526
after filter && map: 1633438922588
after sum: 1633438930620
14
可以看到,上述代码验证了“中间操作都是惰性的”:打印“start”和打印“after filter && map”之间只隔了几十毫秒,而打印“after sum”则在8秒之后,证明了只有在遇到 sum 操作后,filter 和 map 中定义的函数才真正被执行。
生成一个stream对象
Java 8中,引入了4个stream的接口:Stream、IntStream、LongStream、DoubleStream,分别对应Object类型,以及基础类型int、long和double。如下图所示:
在Java中,与stream相关的操作基本都是通过上述的4个接口来实现的,不会涉及到具体的stream实现类。要得到一个stream,通常不会手动创建,而是调用对应的工具方法。
常用的工具方法包括:
- Collection方法:Collection.stream() 或 Collection.parallelStream()
- 数组方法:Arrays.stream(Object[])
- 工厂方法:Stream.of(Object[]), IntStream.range(int, int) 或 Stream.iterate(Object, UnaryOperator) 等等
- 读取文件方法:BufferedReader.lines()
- 类 java.nio.file.Files 中,也提供了Stream相关的API,例如 Files.list, Files.walk 等等
Stream的基本操作
我们以接口Stream为例,先介绍stream的一些基本操作。
forEach()
Stream中的forEach方法和Collection中的forEach方法相似,都是对每个元素执行指定的操作。
forEach方法签名为:
void forEach(Consumer<? super T> action)
forEach方法是一个终止操作,意味着在它之前的所有中间操作都将会被执行,然后再马上执行 action 。
filter()
filter方法的方法签名是:
Stream<T> filter(Predicate<? super T> predicate)
filter方法是一个中间操作,它的作用是根据参数 predicate 过滤元素,返回一个只包含满足predicate条件元素的Stream。
示例代码:
public static void filterStream() {
List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
words.stream()
.filter(s -> s.length() > 3) // 过滤掉长度小于等于3的单词
.forEach(s -> System.out.println(s));
}
上述代码输出为:
hello
world
love
limit()
limit方法签名为:
Stream<T> limit(long maxSize);
limit方法是一个短路型(short-circuiting)的中间操作,作用是将当前的Stream截断,只留下最多 maxSize 个元素组成一个新的Stream。短路型(short-circuiting)的含义是指将一个无限元素的Stream转换为一个有限元素的Stream。
例如,Random.ints 可以生成一个近似无限的随机整数流,我们可以通过limit方法限制生成随机整数的个数。示例代码:
public static void limitStream() {
Random random = new Random();
// 打印左闭右开区间中 [1, 100) 中的 5 个随机整数
random.ints(1, 100)
.limit(5)
.forEach(System.out::println);
}
上述代码的输出类似:
90
31
31
52
63
distinct()
distinct的方法签名是:
Stream<T> distinct();
distinct是一个中间操作,作用是返回一个去除重复元素后的Stream。
作者曾遇到过一个有趣的场景:要生成10个不重复的随机数字。可以结合Random.ints (Random.ints 可以生成一个近似无限的随机整数流)方法来实现这个需求。示例代码如下:
public static void distinctStream() {
Random random = new Random();
// 在左闭右开区间中 [1, 100) 随机生成 10 个不重复的数字
random.ints(1, 100)
.distinct()
.limit(10)
.forEach(System.out::println);
/*
// 一个有趣的问题,如果 limit 方法放在 distinct 前面,
// 结果和上面的代码有什么区别吗?
// 欢迎加群讨论。
random.ints(1, 100)
.limit(10)
.distinct()
.forEach(System.out::println);
*/
}
sorted()
sorted的方法签名有两个,分别是:
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
前者是按照自然顺序排序,后者是根据指定的比较器进行排序。
sorted方法是一个中间操作,和Collection.sort方法作用相似。
示例代码如下:
public static void sortedStream() {
List<String> list = Arrays.asList("Guangdong", "Fujian", "Hunan", "Guangxi");
// 自然排序
list.stream().sorted().forEach(System.out::println);
System.out.println("===============");
// 对省份进行排序,首先按照长度排序,如果长度一样,则按照字母顺序排序
list.stream().sorted((first, second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}).forEach(System.out::println);
}
上述代码的输出为:
Fujian
Guangdong
Guangxi
Hunan
===============
Hunan
Fujian
Guangxi
Guangdong
结语
欢迎来到 Java 的函数式编程世界!!!
本文介绍了 Stream 的概念和基本操作。大家尤其要理解中间操作和终止操作的概念。
认真阅读完本文后,你应该对 Stream 有了一个初步的认识,但这只是 Stream 编程的入门,更有趣更有挑战性更有可玩性的还是随后即将要介绍的 map-reduce 操作。
Java中的函数式编程(六)流Stream基础的更多相关文章
- Java 中的函数式编程(Functional Programming):Lambda 初识
Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...
- Java中的函数式编程(八)流Stream并行编程
写在前面 在本系列文章的第一篇,我们提到了函数式编程的优点之一是"易于并发编程". Java作为一个多线程的语言,它通过 Stream 来提供了并发编程的便利性. 题外话: 严格来 ...
- Java中的函数式编程(七)流Stream的Map-Reduce操作
写在前面 Stream 的 Map-Reduce 操作是Java 函数式编程的精华所在,同时也是最为复杂的部分.但一旦你啃下了这块硬骨头,那你就真正熟悉Java的函数式编程了. 如果你有大数据的编程经 ...
- Java中的函数式编程(二)函数式接口Functional Interface
写在前面 前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是"第一等公民(first-class citizens)".函数是"第一等公 ...
- Java中的函数式编程(五)Java集合框架中的高阶函数
写在前面 随着Java 8引入了函数式接口和lambda表达式,Java 8中的集合框架(Java Collections Framework, JCF)也增加相应的接口以适应函数式编程. 本文的 ...
- Java中的函数式编程(三)lambda表达式
写在前面 lambda表达式是一个匿名函数.在Java 8中,它和函数式接口一起,共同构建了函数式编程的框架. lambda表达式乍看像是匿名内部类的一种语法糖,但实际上,它们是两种本质不同的事物 ...
- Java中的函数式编程(四)方法引用method reference
写在前面 我们已经知道,lambda表达式是一个匿名函数,可以用lambda表达式来实现一个函数式接口. 很自然的,我们会想到类的方法也是函数,本质上和lambda表达式是一样的,那是否也可以用类 ...
- 随便聊聊 Java 8 的函数式编程
函数式编程(Functional Programming) 首先,我们来了解一个叫做"编程范式"的概念. 什么是"编程范式"呢?简单来说就是指导我们编程的方法论 ...
- 读懂Java中的Socket编程
Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的S ...
随机推荐
- 查询同一张表符合条件的某些数据的id拼接成一个字段返回
同一张表存在类似多级菜单的上下级关系的数据,查询出符合条件的某些数据的id拼接成一个字段返回: SELECT CONCAT(a.pid, ',', b.subid) AS studentIDS FRO ...
- python生成时间序列(date_range)
介绍 自己写了一个用python内置模块实现的生成时间序列的函数 支持自动推断字符串到datetime的转换, 但对格式有一定要求, 其它格式可手动指定格式化方式, 格式化方式与python内置格式化 ...
- awk的执行方式
https://blog.csdn.net/fengyuanye/article/details/82858863 awk执行有三种形式: 1.直接以命令行来执行, 语法形式为:awk ...
- 【C++基础教程】第五课
上次的作业答案,非常简单. 第一题: 我们需要知道,字符(char类型)在计算机中存储的时候,是把这个字符对应的代码(专业术语叫做编码)进行存储.例如,换行符'\n'的代码就是10,'0'对应的代码就 ...
- 【C++基础教程】第二课
一,上次的课后练习答案 1,输出1+2=3 2,输出2 2.25 2.25 2.25 3,第一空iostream或bits/stdc++.h 第二空main(),main(void)或main(int ...
- PHP中操作数据库的预处理语句
今天这篇文章的内容其实也是非常基础的内容,不过在现代化的开发中,大家都使用框架,已经很少人会去自己封装或者经常写底层的数据库操作代码了.所以这回我们就来复习一下数据库中相关扩展中的预处理语句内容. 什 ...
- learn git(远程仓库github)
|由于本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置: 第1步:创建SSH Key.在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa ...
- 开源ASR服务器vosk
概述 近几年由于AI的迅速发展,语音相关的自然语言处理NLP项目也变多了,新的技术也越来越成熟,其中TTS(语音生成)和ASR(语音识别)是NLP中非常重要的环节. 今天我们介绍一个开源的ASR项目v ...
- 使用uView UI+UniApp开发微信小程序--微信授权绑定和一键登录系统
在前面随笔<使用uView UI+UniApp开发微信小程序>和<使用uView UI+UniApp开发微信小程序--判断用户是否登录并跳转>介绍了微信小程序的常规登录处理和验 ...
- dubbo微服务架构
架构 节点角色说明 调用关系说明 服务容器负责启动,加载,运行服务提供者. 服务提供者在启动时,向注册中心注册自己提供的服务. 服务消费者在启动时,向注册中心订阅自己所需的服务. 注册中心返回服务提供 ...