1.函数式编程
函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。
λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
在函数式编程中,函数是第一类对象(第一类公民),意思是说一个函数,既可以作为其它函数的参数(输入值),也可以从函数中返回(输入值),被修改或者被分配给一个变量。
第一类对象不一定是面向对象程序设计所指的物件,而可以指任何程序中的实体。一般第一类对象所特有的特性为:
- 可以被存入变数或其他结构
- 可以被作为参数传递给其他函数
- 可以被作为函数的返回值
- 可以在执行期创造,而无需完全在设计期全部写出
- 即使没有被系结至某一名称,也可以存在
函数式编程的理论基础是Lambda演算,其本身是一种数学的抽象但不是编程语言
函数式编程关心数据的映射,命令式编程关心解决问题的步骤
2.响应式编程
响应式编程使用三个核心概念:数据流,函数式编程和异步观察。
反应式编程(reactive programming )是基于 函数式编程(functional programming )的, 所以也可以叫做 函数反应式编程 (functional reactive programming)
FRP基本上就是面向异步事件流的编程了,这个异步事件流叫:Observable,一般叫Stream
Stream就是一个 按时间排序的Events(Ongoing events ordered in time)序列
Stream是不可变(Immutability)的,任何操作都返回新的Stream, 且它是一个Monad(它有map和flatMap方法)。
FRP的关注点在Stream,而FP的关注点在(Type, Operate),Stream -> (Type, Operate)是一种泛化(generic),(Type, Operate) -> Stream 是一种派生。
RP本身是建立于观察者模式之上的一种编程范式(级别同MV*),FP则更偏向底层解决一般化问题。引用wiki上的一句话:
Functional reactive programming (FRP) is a programming paradigm for reactive programming on functional programming.
同样由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件.因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归.
3.Java Streams
Java 8 中的Stream表示的是元素的序列。流中的元素可能是对象、int、long 或 double 类型。流作为一个高层次的抽象,并不关注流中元素的来源或是管理方式
Stream 是表示流的接口,T 是流中元素的类型。对于基础类型的流,可以使用专门的类 IntStream、LongStream 和 DoubleStream。
Stream和Collections有如下差异:
- Stream不存储数据,Stream不是保存元素的数据结构,相应的,它通过一系列计算操作从数据结构,数组,生成器功能或I / O通道等源中传递元素。
- Stream本质上支持functional,一个流的操作会生成一个结果,但是不是改变原始数据
- 延迟寻找(Laziness-seeking) 流的很多操作,如filtering, mapping, or duplicate removal,可以被延时执行。中间操作总是延时执行
- Stream可能是无界的 collections必须有大小,Stream不需要
- 只有消费一次(Consumable) 流的元素在流的生存期内仅被访问一次
Stream 的创建方式有:
- 通过Collection的stream和parallelStream方法
- 通过Arrays.Stream(Object[])方法
- 通过Stream的静态工厂方法,如Stream.of(Object[]), IntStream.range(int, int) or Stream.iterate(Object, UnaryOperator);
- 可以从BufferedReader.lines方法获得文件的行。
- 文件路径流可以从Files中的方法获得
- 可以从Random.ints获得随机数流
- JDK中的许多其他流承载方法,包括BitSet.stream、Pattern.splitAsStream(java.lang.CharSequence), and JarFile.stream().
流的操作可以顺序执行或并行执行, 后者可以获得比前者更好的性能。但是如果实现不当,可能由于数据竞争或无用的线程同步,导致并行执行时的性能更差。
- 一个流是否会并行执行,可以通过其方法isParallel()来判断。
- 根据流的创建方式,一个流有其默认的执行方式。可以使用方法sequential()或parallel()来将其执行方式设置为顺序或并行。
3.1 流水线
流操作分为2类:
- 中间操作(intermediate operation) 可以连接起来的流操作,类似filter/map,会返回一个新的stream。注意中间操作是延时执行的,只有当终结操作执行的时候才会触发
- 终结操作(terminal operation) 关闭流的操作,触发中间操作执行
不同的流操作以级联的方式形成处理流水线。一个流水线由
- source:一个源(source)
- intermediate operation:0 到多个中间操作(intermediate operation)
- terminal operation:一个终结操作(terminal operation)完成。
流中间操作在应用到流上,返回一个新的流。下面列出了常用的流中间操作:
- map:通过一个 Function 把一个元素类型为 T 的流转换成元素类型为 R 的流。
- flatMap:通过一个 Function 把一个元素类型为 T 的流中的每个元素转换成一个元素类型为 R 的流,再把这些转换之后的流合并。
The Java Stream flatMap() methods maps a single element into multiple elements
- filter:过滤流中的元素,只保留满足由 Predicate 所指定的条件的元素。
- distinct:使用equals方法来删除流中的重复元素。
- limit:截断流使其最多只包含指定数量的元素。
- skip:返回一个新的流,并跳过原始流中的前 N 个元素。
- sorted:对流进行排序。
- peek:返回的流与原始流相同。当原始流中的元素被消费时,会首先调用peek方法中指定的 Consumer 实现对元素进行处理。
3.1.1 map和flatMap
static void mapAndFlatMap() {
// map 一对一的映射关系
long count = Arrays.stream(strStream)
.map(String::toLowerCase)
.count();
System.out.println("count = " + count);
// flatMap 支持一对多的映射关系
Stream.of(1,2,3)
.map(v -> v+1)
.flatMap(v -> Stream.of(v*5,v*10))
.forEach(System.out::println);
// flatMap 把Stream<String []> 转换成Stream<String> ,然后合并成一个Stream返回
List<String> uniqueChar = Arrays.stream(strStream)
.map(s -> s.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());
System.out.println("uniqueChar list = "+ uniqueChar);
}
3.1.2 limit和skip方法
static void limitAndSkip() {
// 截断流使其最多只包含指定数量的元素
Arrays.stream(intStream)
.limit(2)
.forEach(System.out::println);
// 返回一个新的流,并跳过原始流中的前 N 个元素
Arrays.stream(intStream)
.skip(2)
.forEach(System.out::println);
}
3.1.3 filter和distinct
流操作可以是有状态或无状态的,当一个有状态的操作在处理一个元素时,它可能需要使用处理之前的元素时保留的信息;无状态的操作可以独立处理每个元素
distinct 和 sorted 是有状态操作的例子
- distinct 操作从流中删除重复元素,它需要记录下之前已经遇到过的元素来确定当前元素是否应该被删除。
- sorted 操作对流进行排序,它需要知道所有元素来确定当前元素在排序之后的所在位置。
filter 和 map 是无状态操作的例子
- filter 操作在进行过滤时只需要看当前元素即可。
- map 操作可以独立转换当前元素。一般来说,有状态操作的运行代价要高于无状态操作,因为需要额外的空间保存中间状态信息。
static void filterAndDistinct() {
// filter 过滤流中的元素,只保留满足由 Predicate 所指定的条件的元素
Arrays.stream(intStream)
.filter(i -> i % 2 == 0)
.forEach(System.out::println);
// 使用equals方法来删除流中的重复元素
Arrays.stream(intStream)
.distinct()
.forEach(System.out::println);
}
终结操作会生成一个结果(result)或副作用(side-effect) . Stream只能遍历一次,遍历完后,这个Stream就是被消费掉了,不能再使用。如果需要重新遍历,则需要创建一个新的流
常见的终结操作
- foreach 消费流程中每个元素,并对其应用Lambda
- foreachOrdered 按照流的相遇顺序来处理元素,如果流有确定的相遇顺序的话,并对其应用Lambda
- reduce 把一个流约简成单个结果
- count 返回流中元素的数量
- anyMatch 检查流中的任意元素满足给定的条件,判断的条件由 Predicate 指定
- allMatch 检查流中的全部元素满足给定的条件,判断的条件由 Predicate 指定
- noneMatch 检查流中的没有元素=满足给定的条件,判断的条件由 Predicate 指定
- findAny 查找流中的任意一个元素,返回Optional 对象
- findFirst 查找流中的第一个元素,返回Optional 对象
- collect 把流归约成一个集合,如List/Map
- min 求流中元素的最小值
- max 求流中元素的最大值
3.2 encounter order 流的相遇顺序
一个流的相遇顺序(encounter order)是流中的元素被处理时的顺序
流根据其特征可能有,也可能没有一个确定的相遇顺序
- 从 ArrayList 创建的流有确定的相遇顺序;
- 从 HashSet 创建的流没有确定的相遇顺序。
大部分的流操作会按照流的相遇顺序来依次处理元素。如果一个流是无序的,同一个流处理流水线在多次执行时可能产生不一样的结果。
比如 Stream 的 findFirst() 方法获取到流中的第一个元素。如果在从 ArrayList 创建的流上应用该操作,返回的总是第一个元素;如果是从 HashSet 创建的流,则返回的结果是不确定的
对于一个无序的流,可以使用 sorted 操作来排序;对于一个有序的流,可以使用 unordered() 方法来使其无序。
find和match
static void findAndMatch() {
boolean anyMatch = Arrays.stream(strStream)
.anyMatch(v -> v.equals("hello"));
System.out.println("anyMatch = "+ anyMatch);
boolean allMatch = Arrays.stream(strStream)
.allMatch(v -> v.equals("hello"));
System.out.println("allMatch = "+ allMatch);
boolean noneMatch = Arrays.stream(strStream)
.noneMatch(v -> v.equals("hello"));
System.out.println("noneMatch = "+ noneMatch);
// findFirst在有相遇顺序的流中,总是返回的第一个;在没有相遇顺序的流中,每次可能返回的值不一样
OptionalInt firstValue= Arrays.stream(intStream)
.filter(i -> i % 3 ==0)
.findFirst();
firstValue.ifPresent(System.out::println);
// 在并发场景下,使用findAny的限制比findFirst要少
OptionalInt anyValue= Arrays.stream(intStream)
.filter(i -> i % 3 ==0)
.findAny();
anyValue.ifPresent(System.out::println);
}
3.3 Optional 和 短路求值
Optional类一个容器,代表一个值存在或不存在,使用此类可以避免和null相关的Bug
- isPresent 将在Optional包含值的时候返回true,否则返回false
- ifPresent(Consumer block) 值存在的时候会执行给定的代码块
短路求值:Java中&&和||运算符在流中的版本,有些操作不需要处理整个流就能得到结果,如anyMatch 、findFirst等,找到一个值就可以有结果了
reduce、max和 min
reduce操作把一个流约简成单个结果。约简操作可以有 3 个部分组成:
- 初始值:在对元素为空的流进行约简操作时,返回值为初始值。
- 叠加器:接受 2 个参数的BiFunction。第一个参数是当前的约简值,第二个参数是当前元素,返回结果是新的约简值。
- 合并器:对于并行流来说,约简操作可能在流的不同部分上并行执行。合并器用来把部分约简结果合并为最终的结果。
看描述比较抽象,看代码里面的注释:
依次对流中的每个元素进行accumulator.apply操作,把上一次的结果作为下一次的输入参数,返回最后的结果
static void reduceAndMaxMin() {
/*
* Optional<T> reduce(BinaryOperator<T> accumulator) 等效如下代码
* {
* boolean foundAny = false;
* T result = null;
* for (T element : this stream) {
* if (!foundAny) {
* foundAny = true;
* result = element;
* } else
* result = accumulator.apply(result, element);
* }
* return foundAny ? Optional.of(result) : Optional.empty();
* }
*/
OptionalInt sum1 = Arrays.stream(intStream)
.reduce((i,j) -> i+j);
sum1.ifPresent(System.out::println);
/* T reduce(T identity, BinaryOperator<T> accumulator) 等效于
* {
* U result = identity;
* for (T element : this stream)
* result = accumulator.apply(result, element)
* return result;
* }
*/
int sum2 = Arrays.stream(intStream)
.reduce(0,(i,j) -> i+j);
System.out.println(sum2);
/**
* <U> U reduce(U identity,
* BiFunction<U, ? super T, U> accumulator,
* BinaryOperator<U> combiner);
*
* 等效如下代码
* {
* U result = identity;
* for (T element : this stream)
* result = accumulator.apply(result, element)
* return result;
* }
* 但并不被限制按顺序执行,可能并发执行,combiner必须兼容accumulator
* combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
*/
int sum3 = Stream.of(1, 2, 3, 4, 5)
.parallel()
.reduce(0, (i, j) -> i + j, (i, j) -> i + j);
System.out.println(sum3);
// max求流中元素的最大值
OptionalInt max = Arrays.stream(intStream)
.max();
max.ifPresent(System.out::println);
// min求流中元素的最小值
OptionalInt min = Arrays.stream(intStream)
.min();
min.ifPresent(System.out::println);
// 返回流中元素的数量
long count = Arrays.stream(intStream)
.count();
System.out.println(count);
}
collect 操作 表示的是另外一类的约简操作。与 reduce 不同在于,collect 会把结果收集到可变的容器中,如 List 或 Set。
收集操作通过接口 java.util.stream.Collector 来实现。Java 已经在类 Collectors 中提供了很多常用的 Collector 实现
3.4 Spliterator 和 Collector 区别简介
Spliterator 需要绑定到流之后才能遍历其中的元素,
Spliterator basically means “splittable Iterator”.
Single thread can traverse/process the entire Spliterator itself, but the Spliterator also has a method trySplit() which will “split off” a section for someone else (typically, another thread) to process – leaving the current spliterator with less work.
Collector combines the specification of a reduce function (of map-reduce fame), with an initial value, and a function to combine two results (thus enabling results from Spliterated streams of work, to be combined.)
参考链接
- https://www.jcp.org/en/jsr/detail?id=335
- https://stackoverflow.com/questions/19235606/understanding-spliterator-collector-and-stream-in-java-8
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
- https://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html
- https://www.zhihu.com/question/36431501
- https://stackoverflow.com/questions/30216979/difference-between-java-8-streams-and-rxjava-observables
- https://www.zhihu.com/question/28292740
- https://developer.ibm.com/zh/articles/j-understanding-functional-programming-1/
- https://developer.ibm.com/zh/articles/j-understanding-functional-programming-3/