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 ...
随机推荐
- Windows下Rancher复制Pod内文件到本地
Rancher 未提供直接获取 Pod 内文件的工具(如果有请评论告知下,蟹蟹),但提供了 Rancher 的 CLI 客户端,通过 CLI 可以调用 k8s 的 CLI (kubectl) 命令来操 ...
- Linux复习笔记-001-进程的管理
1.什么是进程? 进程是已经启动的可执行的程序运行实例. 程序是二进制文件,静态 ./bin/date/ /usr/sbin/ 进程:是程序运行的过程 2.Linux为1的进程? centos5或6为 ...
- DH算法图解+数学证明
前几天和同事讨论IKE密钥交换流程时,提到了Diffie-Hellman交换.DH算法最主要的作用便是在不安全的网络上成功公共密钥(并未传输真实密钥).但由于对于DH算法的数学原理则不清楚,因此私下对 ...
- Asp.net Core Jwt简单使用
.net 默认新建Api项目不需要额外从Nuget添加Microsoft.AspNetCore.Authentication.JwtBearer appsettings.json { "Lo ...
- 5.10学习总结——Activity的跳转和传值
使用sharedpreference是对信息的存储,也可以进行传值,今天通过查找资料,学习了Activity的跳转和传值方法. 跳转 1.显示跳转 4种方法 1 2 3 4 5 6 7 8 9 10 ...
- 一起学习PHP中GD库的使用(一)
又到了一个大家非常熟悉的库了,对于图像图形的处理来说,GD 库是 PHPer 们绕不过去的一道坎.从很早很早的 CMS 或者 Discuz 时代,各类开源软件在安装的时候就会明确地指出 GD 库是它们 ...
- Linux系列(12) - find
简述 find搜索文件,搜索方式丰富,遍历给定范围的所有目录下的文件(避免大范围的搜索,会非常浪费系统资源,建议不在直接在"/"目录下搜索) 命令格式 基本使用 格式:find [ ...
- 如何理解 jmeter 的线程数与并发数之间的关系
https://blog.csdn.net/weixin_39955351/article/details/110548162 多个线程组的并发是如何计算的?
- javascript 实现php str_pad
* 查看php.net官方手册 string str_pad ( string $input , int $pad_length [, string $pad_string = " &quo ...
- P4548-[CTSC2006]歌唱王国【概率生成函数,KMP】
正题 题目链接:https://www.luogu.com.cn/problem/P4548 题目大意 \(t\)次询问,给出一个长度为\(m\)的串\(S\)和一个空串\(T\),每次在\(T\)后 ...