Java函数式编程:三、流与函数式编程
本文是Java函数式编程的最后一篇,承接上文:
Java函数式编程:一、函数式接口,lambda表达式和方法引用
Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化
前面都是概念和铺垫,主要讲述了函数式编程中,如何获取我们需要的函数作为参数或输出来进行编程,同时补充了一些要注意的知识。比如柯里化,闭包等等。
而这一篇要讲的是Java函数式编程的主菜,也就是如何把我们苦苦获取的函数,运用在真正的对于数据的处理之中。
在以前,我们通常会通过一个集合把这些数据放在一起,然后详细编写其处理过程使之能被逐一处理,最后再通过一个集合把它们获取出来,这没有任何问题。但是对于某些情况下而言,我们已完全洞悉并且厌烦了这些处理过程,我们渴望获得一种更轻便,更简易的手段,能使得整个集合中的数据处理就像水流通过管道一样,我们可以随意在这条管道上拼接各式各样的制式的处理器来处理这些数据,并最后给出一个结果。
——这个制式的处理器就是我们的函数,而这个管道就是流
流
流是一个与任何特定的存储机制都没有关系的元素序列,我们一般会这样说流:没有存储。
不同于对于任何一个集合的操作,当我们使用流时,我们是从一个管道中抽取元素进行处理,这非常重要,因为大多数时候我们不会无缘无故的将元素放进一个集合,我们一定是希望对其进行一些处理,也就是说,我们不是为了存储才将它们放入集合的。
如果是这样,那么就意味着我们的编程很多时候需要转向流而不是集合。
流最关键的优点是,能够使得我们的程序更小也更好理解。事实上,lambda函数和方法引用正是在这里才发挥出了其真正的威力,它们一同将Java带入了声明式编程:我们说明想要完成什么,而不是指明需要怎么去做。
- 类似流+函数式编程这样实现的声明式编程机制,就被称之为内部迭代,我们看不见其内部的具体操作
- 而通过循环,将内部的数据一个一个处理成型的机制就被称为外部迭代,我们可以显式的看清和修改内部的操作
流带来的声明式编程是Java 8最重要的新特性之一,为此,Java还引入了新的关键词default以便它们大刀阔斧的修改一些老的集合类,以便使得它们支持流。
下面,我们将分三个阶段来了解,我们可以怎样去使用流,并运用流和函数式编程获得极佳的编程体验
- 流的创建
- 流的中间操作
- 流的终结操作
1、流的创建
最基本的流的创建方法就是
Stream.of(一组条目)Collection.stream()
我们可以把任意相同类型的一组条目写在Stream.of()的参数中使之变成一个流,比如:
Stream.of("a", "b", "c", "d");
Stream.of(new Node(1), new Node(2), new Node(3));
Stream.of(1, 2, 3, 4, 5);
而Collection接口的stream()方法则更是我们的好伙伴,所有实现了该接口的集合,都可以直接转变为一个流由我们处理。
此外,我们还有以下生成流的手段
- 随机数流
int基本类型的区间范围方法generate()方法iterate()方法- 流生成器
Arrays.stream()将数组转换为流- 正则表达式
下面来逐一了解
随机数流
Random类已经得到了增强,现在有一组可以生成流的方法。
ints()longs()doubles()boxed()
可以清楚的看到,我们只能通过Random类获取三种基本类型的流,或者在其后加上boxed()来获取它们的包装类的流。实际上,Random类生成的这些数值,还有别的价值,比如通过随机数来获取某个列表中的随机下表对应值,以此来获取随机的对象。
int区间范围方法
IntStraem类提供了新的range()方法,可以生成一个流,它代表一个由int值组成的序列,对于IntStream.range(a, b)来说,这个流中的数据是[a, b)区间的所有整数。
利用这个方法,我们可以通过流很好的代替某些循环了,比如:
public class Repeat{
public static repeat(int n, Runnable action){
IntStream.range(0, n).forEach(i -> action.run());
}
}
这样一个方法就是把我们的action方法执行n次,可以很好的替代普通的循环。
generate() 方法
Stream.generate()方法可以接受一个方法作为参数,该方法必须要返回一个实例或基本类型。总之,无论你给出的方法返回了什么,generate()方法会无限的根据该方法产生元素并塞入流中,如果你不希望它无限产生,那么你应该使用limit()来限制次数
AtomicInteger i = new AtomicInteger();
Stream.generate(() -> i.getAndIncrement())
.limit(20)
.forEach(System.out::println);
// 输出为从0到19
iterate()方法
顾名思义,这个方法通过迭代不断产生元素,它可以将第一个参数作为输入赋给第二个参数 (也就是那个方法),然后该方法会产生一个输出,随后该输出又会作为输入再度交给方法来产生下一个输出,由此不断迭代。一个典型的例子是由此产生一个斐波那契数列的方法,如下所示。
int x = 0;
public Stream<Integer> numbers(){
return Stream.iterate(1, o ->{
int result = o + x;
x = o;
return result;
});
}
public static void main(String[] args) {
test2 t = new test2();
t.numbers()
.limit(20)
.forEach(System.out::println);
}
流生成器
流生成器方法Stream.builder()可以返回Stream.Builder<T>类,你可以自定义这个返回的类的泛型以便适配需求,随后,你可以将它当作一个类似StringBuilder一样的存在使用,通过add()等方法向里面塞入元素,并最终通过build()方法来返回一个流。
Stream.Builder<String> builder = Stream.builder();
builder.add("a").add("b").add("c").build()
.map(x -> x.toUpperCase())
.forEach(System.out::print);
// 输出ABC
Arrays流方法
Arrays.stream()静态方法可以将一个数组转化为流,非常简单易理解
int[] chars = {1,2,3,4,5};
Arrays.stream(chars)
.forEach(System.out::print);
// 输出12345
正则表达式
Java 8在java.util.regex.Pattern类中加入了一个新方法splitAsStream(),该方法接受一个字符序列并可以根据我们传入的公式将其分拆为一个流。
要注意的是,这个地方的输入不能直接是一个流,必须得是一个CharSequence
String s = "abcdefg";
Pattern.compile("[be]").splitAsStream(s)
.map(x -> x+"?")
.forEach(System.out::print);
// 输出a?cd?fg?
2、中间操作
我们获取了流,那么我们要做什么呢?显然,我们希望逐个对流中的数据进行操作,我们有以下方式可选:
- 查看元素
peek()
- 对元素排序
sorted()sorted(Comparator compa)
- 移除元素
distinct()filter(Predicate)
- 将函数应用于每个元素
map(Function func)mapToInt(ToIntFunction func)mapToLong(ToLongFunction func)mapToDouble(ToDoubleFunction func)
- 应用函数期间组合流
flatMap(Function func)flatMapToInt(ToIntFunction func)flatMapToLong(ToLongFunction func)flatMapToDouble(ToDoubleFunction func)
查看元素
主要就是peek(),它允许我们在不做任何操作的情况下查看流中的所有元素,其意义在于我们可以通过它来跟踪和调试我们的流代码,当你不知道你的代码中,这些流元素究竟被变成了什么样子的话,可以使用这个方法而不是forEach()来终止流。
对元素排序
sorted()方法,同样很好理解,如果你不给Comparator作为参数,那么就是一个很普通的排序方法,类似Arrays.sort()这样,你可以查看源码来看看默认顺序究竟如何。
不过更可靠的方法是我们自己来实现一个Comparator来操控整个流的比较结果。
移除元素
主要有两种方法,分别是distinct()和filter二者都很好用,distinct()可以消除那些重复的元素,这比通过Set来获取元素要便捷得多。
而filter(Predicate)更是全能,该方法需要以一个返回值为布尔的方法为变量,它会负责抛弃那些返回值为false的方法,留下那些返回值为true的方法,可以大大降低我们的代码量。
将函数应用于各个元素
主要就是map(Function func),其他三个方法只是返回值变为对应的基本类型流而已,主要是为了提高效率。我们需要提供一个能够处理流中元素并返回新值的方法,随后该方法就会将我们提供的参数方法应用于每个元素上,十分方便
在应用map()期间组合流
flatMap(),其实和map()的区别就是,有时候我们提供的参数方法会返回一个流而不是一个元素。这样的话,我们就需要另一个方法能够以流为参数进行处理,也就是需要一个方法把我们返回的流平展开成为元素,类似于把所有返回的流拼接在一起,成为一个更大的流然后再进行处理。
一个典型的例子:
public static void main(String[] args){
Stream.of(1, 2, 3)
.flatMap(i -> Stream.of('a', 'b', 'c'))
.forEach(System.out::println);
// 上面的flatMap()处如果使用map()那么会返回三个元素为{a, b, c}的流
// 而如果是faltMap()则返回的是元素为{a, b, c, a, b, c, a, b, c}的流
}
3、Optional类型
到此我们已经了解了流的创建和中间操作,但是在学习终结操作之前,我们还有一个更重要的问题:健壮性研究。
在前面的处理环节我们需要考虑,如果流中存在一个null会发生什么呢?要知道流可不是什么快乐通道,作为程序员,我们必须要考虑周全,环环相扣。
所以为了防止在某些不该出现null的地方出现了null导致处理失败,我们需要一个类似占位符的存在,它既可以作为流元素占位也可以在我们要找的元素不存在时告知我们(即不会抛出异常)
这个想法的实现就是Optional类型,这些类型只会通过某些标准流操作返回,因为这些操作不一定能保证所要的结果一定存在:
findFirst()返回包含第一个元素的Optional,若流为空,则返回Optional.emptyfindAny()返回包含任何元素的Optional,若流为空,则返回Optional.emptymax()和min()分别返回包含流中最大或最小值的Optional,若流为空,则返回Optional.emptyreduce()的其中一个实现,参数为一个接收两个参数并返回一个结果的方法引用,其作用就是返回各个元素根据该参数计算得到的值,其中每次迭代计算出的值会作为下一次计算的第一个参数
比如1,2,3,4给出reduce((x1, x2) -> x1+x2)
那么计算流程会是1+2=3, 3+3=6,6+4=10average()可以对数值化的流计算均值并以对应的Optional类对象返回
现在,我们可以从流中获取Optional对象了,那么有什么用呢?这就要提到便捷函数了
便捷函数可以用于获取Optional中封装的数据,并且简化了步骤
ifPresent(Consumer):如果值存在,则通过该值调用Consumer函数,否则跳过orElse(otherObject):如果值存在,则返回该对象,否则返回参数对象orElseGet(Supplier):如果值存在,则返回该对象,否则返回Supplier方法创造的对象orElseThrow(Supplier):如果值存在,则返回该对象,否则抛出一个使用Supplier方法创造的异常
如果我们需要自己创建Optional对象,那么我们可以使用这些Optional类的静态方法:
empty():返回一个空的Optionalof(value):如果已经知道这个value不是null,可以使用该方法把它封装在一个Optional对象中ofNullable(value):如果不能确定封装值是不是null,则使用此方法封装
最后,还有三种方法支持对Optional进行事后处理,提供最后一次处理机会
filter(Predicate)map(Function)flatMap(Function)
它们的作用都和中间操作中的对应方法一致,只不过返回值会被封装在Optional对象中
最后,回到我们的主角Stream上来,有时候,我们不是给出的参数含有null而是处理的结果可能含有null那么我们可能会希望将这些返回值包含在Optional对象中,那么我们可以通过类似x -> Optional.of(result)这样的方法将其封装,但是,如果这么做了就一定要清楚我们该如何获取这样的流中的对象。请牢记,要先验证是否存在,才能获取
Stream
.filter(Optional::isPresent)
.map(Optional::get) // 到这里,流中的数据就都是Optional对象中包含的值了
// 继续处理
4、终结操作
这些操作接受一个流作为参数,并生成一个最终结果而非返回那个流,因此,只要调用这些方法,流处理就将终结
- 将流转化为一个数组
toArray()toArray(generator)
该方法会将元素保存在generator中,而不是创建一个新的并返回
- 在每个流元素上应用某个终结操作
forEach(Consumer)
在每个元素上调用Consumer方法forEachOrdered(Consumer)
该版本确保对元素的操作顺序是原始的流顺序
- 收集操作
collect(Collector)
相当复杂的一个方法,可以将所有元素存入我们给出的Collector容器中。- 本方法主要复杂在,我们实际上可以使用
java.util.stream.Collectors文档中相当多的对象,而且其中有一部分很复杂
比如如果我们希望放入一个TreeSet中使它们总是有序,那么我们可以使用Collectors.toCollection(TreeSet::new)来创建该容器并应用
- 本方法主要复杂在,我们实际上可以使用
collect(Supplier, BiConsumer, BiConsumer)- 在极小情况下,我们无法从
Collectors类中找到我们想要的处理容器,那么就需要第二个方法
- 在极小情况下,我们无法从
- 组合所有的流元素
reduce(BinaryOperator)
组合所有元素,组合的方法就是参数方法reduce(identity, BinaryOperator)
以identity为初始值组合所有元素,方法为第二个参数reduce(identity, BiFunction, BinaryOperator)
复杂,未作介绍
- 匹配,都是根据Predicate返回一个布尔值
allMatch(Predicate)anyMatch(Predicate)noneMatch(Predicate)
- 选择一个元素
findFirst()
返回一个包含流中第一个元素的Optional对象,若流中没有元素即返回Optional.emptyfindAny()
返回一个包含流中任意一个元素的Optional对象,若流中没有元素则为Optional.empty- 不过需要注意的是,该方法对于非并行的流似乎总是会选择流中的第一个元素,如果是并行的则随机
- 获取流相关的信息
count()
计算流中元素数量max(Comparator)
通过Comaprator获取流中最大的元素min(Comparator)
通过Comparator获取流中最小的元素- 如果是数值化的流,除了上面这些,还有以下方法
average()
获得平均值sum()
获得累加值summaryStatics()
返回可能有用的摘要数据,基本没什么用
Java函数式编程:三、流与函数式编程的更多相关文章
- Java学习之IO流及网络编程
一.字节 1.1字节输入流(java.io.InputStream) 此抽象类是表示字节输入流的所有类的超类 1.1.1定义了所有子类共性的方法: int read() 从输入流中读取数据的下 ...
- java基础知识三 流
Java 流(Stream).文件(File)和IOJava.io 包几乎包含了所有操作输入.输出需要的类.所有这些流类代表了输入源和输出目标. Java.io 包中的流支持很多种格式,比如:基本类型 ...
- C# TCP应用编程三 异步TCP应用编程
利用TcpListener和TcpClient类在同步方式下接收.发送数据以及监听客户端连接时,在操作没有完成之前一直处于阻塞状态,这对于接受.发送数据量不大的情况或者操作勇士较短的情况下是比较方便的 ...
- Java中的函数式编程(八)流Stream并行编程
写在前面 在本系列文章的第一篇,我们提到了函数式编程的优点之一是"易于并发编程". Java作为一个多线程的语言,它通过 Stream 来提供了并发编程的便利性. 题外话: 严格来 ...
- [零]java8 函数式编程入门官方文档中文版 java.util.stream 中文版 流处理的相关概念
前言 本文为java.util.stream 包文档的译文 极其个别部分可能为了更好理解,陈述略有改动,与原文几乎一致 原文可参考在线API文档 https://docs.oracle.com/jav ...
- [一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念
本文是针对于java8引入函数式编程概念以及stream流相关的一些简单介绍 什么是函数式编程? java程序员第一反应可能会理解成类的成员方法一类的东西 此处并不是这个含义,更接近是数学上的 ...
- Java编程的逻辑 (92) - 函数式数据处理 (上)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java经典类库-Guava中的函数式编程讲解
如果我要新建一个java的项目,那么有两个类库是必备的,一个是junit,另一个是Guava.选择junit,因为我喜欢TDD,喜欢自动化测试.而是用Guava,是因为我喜欢简洁的API.Guava提 ...
- 简学Python第三章__函数式编程、递归、内置函数
#cnblogs_post_body h2 { background: linear-gradient(to bottom, #18c0ff 0%,#0c7eff 100%); color: #fff ...
随机推荐
- 关于virtio_net网卡命名的小问题
最近看了一个小问题,涉及到一致性网络设备命名(Consistent Network Device Naming),在此记录一下. 系统是 4.18.0-240.el8.x86_64,centos 8. ...
- java.lang.UnsatisfiedLinkError报错
是因为使用maven时,运行web项目时,在maven的依赖包没有打包到tomcat中(out目录中),所以要手动加上
- 理解C++函数指针和指针函数(一)
函数指针 实际上使用最多的还是指针函数,但我们还是可以先看看函数指针 奇怪的是,大家搜索指针函数,或者Pointer function,出来的还是函数指针的链接. OK,废话不多说,先给大家举个例子. ...
- 基于ASP.NET Core 6.0的整洁架构
大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 本节将介绍基于ASP.NET Core的整洁架构的设计理念,同时基于理论落地的代码 ...
- 微服务网关Gateway实践总结
有多少请求,被网关截胡: 一.Gateway简介 微服务架构中,网关服务通常提供动态路由,以及流量控制与请求识别等核心能力,在之前的篇幅中有说过Zuul组件的使用流程,但是当下Gateway组件是更常 ...
- 微信小程序语音提示
一. 老规矩, 先上demo图: 然后通过 wx.createInnerAudioContext 创建内部 audio 上下文 InnerAudioContext 对象 就能播放 filename ...
- Netty 学习(一):服务端启动 & 客户端启动
Netty 学习(一):服务端启动 & 客户端启动 作者: Grey 原文地址: 博客园:Netty 学习(一):服务端启动 & 客户端启动 CSDN:Netty 学习(一):服务端启 ...
- 从源码中理解Spring Boot自动装配原理
个人博客:槿苏的知识铺 一.什么是自动装配 SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot在启动时会扫描外部引用jar包中的META-INF/spring.factori ...
- 1.云原生之Docker容器技术基础知识介绍
转载自:https://www.bilibili.com/read/cv15180540/?from=readlist
- Elasticsearch:Smart Chinese Analysis plugin
Smart Chinese Analysis插件将Lucene的Smart Chinese分析模块集成到Elasticsearch中,用于分析中文或中英文混合文本. 支持的分析器在大型训练语料库上使用 ...