写在前面

如果说函数式接口和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,通常不会手动创建,而是调用对应的工具方法。

常用的工具方法包括:

  1. Collection方法:Collection.stream() 或 Collection.parallelStream()
  2. 数组方法:Arrays.stream(Object[])
  3. 工厂方法:Stream.of(Object[]), IntStream.range(int, int) 或 Stream.iterate(Object, UnaryOperator) 等等
  4. 读取文件方法:BufferedReader.lines()
  5. 类 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基础的更多相关文章

  1. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  2. Java中的函数式编程(八)流Stream并行编程

    写在前面 在本系列文章的第一篇,我们提到了函数式编程的优点之一是"易于并发编程". Java作为一个多线程的语言,它通过 Stream 来提供了并发编程的便利性. 题外话: 严格来 ...

  3. Java中的函数式编程(七)流Stream的Map-Reduce操作

    写在前面 Stream 的 Map-Reduce 操作是Java 函数式编程的精华所在,同时也是最为复杂的部分.但一旦你啃下了这块硬骨头,那你就真正熟悉Java的函数式编程了. 如果你有大数据的编程经 ...

  4. Java中的函数式编程(二)函数式接口Functional Interface

    写在前面 前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是"第一等公民(first-class citizens)".函数是"第一等公 ...

  5. Java中的函数式编程(五)Java集合框架中的高阶函数

    写在前面 随着Java 8引入了函数式接口和lambda表达式,Java 8中的集合框架(Java Collections Framework, JCF)也增加相应的接口以适应函数式编程.   本文的 ...

  6. Java中的函数式编程(三)lambda表达式

    写在前面 lambda表达式是一个匿名函数.在Java 8中,它和函数式接口一起,共同构建了函数式编程的框架.   lambda表达式乍看像是匿名内部类的一种语法糖,但实际上,它们是两种本质不同的事物 ...

  7. Java中的函数式编程(四)方法引用method reference

    写在前面 我们已经知道,lambda表达式是一个匿名函数,可以用lambda表达式来实现一个函数式接口.   很自然的,我们会想到类的方法也是函数,本质上和lambda表达式是一样的,那是否也可以用类 ...

  8. 随便聊聊 Java 8 的函数式编程

    函数式编程(Functional Programming) 首先,我们来了解一个叫做"编程范式"的概念. 什么是"编程范式"呢?简单来说就是指导我们编程的方法论 ...

  9. 读懂Java中的Socket编程

    Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的S ...

随机推荐

  1. Mysql常用基础命令操作

    常见操作命令:1.连接Mysql (客户端工具NaviCat.phpMyAdmin.MySQL-Front)格式: mysql -h 主机地址 -u用户名 -p用户密码(1)连接到本机上的MYSQL. ...

  2. Django——cookie保持登录

    普通登录时,无法保持登录的状态,每一次请求时都需要重新登录. 而在登录时,生成cookie并保存在浏览器中,这样每次登录就会携带登录信息,就可以保持登录状态了. 操作cookie语法: # (1) 设 ...

  3. java基础之ThreadLocal

    早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编写出优美的多线程程序.Thr ...

  4. Pytest系列(22)- allure的特性,@allure.link()、@allure.issue()、@allure.testcase()的详细使用

    如果你还想从头学起Pytest,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1690628.html 前言 上一篇文章介绍了两种allu ...

  5. HearthbuddyHelper已经开源

    https://github.com/V-arc/HearthbuddyHelper 只是为了稳定,没耗时多久写的一个中控,只是在逐渐堆加,并未进行重构. 通过写这个感受到自己对异步和wpf的理解还有 ...

  6. 企业快速开发平台Spring Cloud+Spring Boot+Mybatis+ElementUI 实现前后端分离

    鸿鹄云架构一系统管理平台 鸿鹄云架构[系统管理平台]使用J2EE技术来实施,是一个大型分布式的面向服务的JavaEE体系快速研发平台,基于模块化.服务化.原子化.热部署的设计思想,使用成熟领先的无商业 ...

  7. 进程代数CSP基础知识总结(Communicating sequencing process)

    进程代数(Process Algebra) Process Algebra 理论 提出者 理论名称 缩写 论文链接 简介 C. A. R. Hoare/Tony Hoare Communicating ...

  8. PHP的CLI命令行运行模式浅析

    在做开发的时候,我们不仅仅只是做各种网站或者接口,也经常需要写一些命令行脚本用来处理一些后端的事务.比如对数据进行处理统计等.当然也是为了效率着想,当一个事务有可能会有较长的耗时时,往往会交由服务器的 ...

  9. Mysql将其他表中的数据更新到指定表中

    update tb  set tb.字段= (select 字段 from tb1 where tb.字段1 = tb1.字段1); update role set uid = (select ID ...

  10. ecshop调用商品原图的方法

    一.在列表页可以用{$goods.goods_thumb}   {$goods.goods_img}调用缩略图和商品图片 就是没有调用原图的下面是修改category.php 总sql语句处替换成下面 ...