前言:函数式编程在其他语言中应用非常广泛,比如python、scala,java直到jdk1.8才引入,最近工作中用到较多,从实用型的角度总结函数式编程的用法。
文章目录
- 1. 什么是函数式编程?
- 2. 如何用?
- 3. Lambda表达式
- 4. 流(Stream)
- 1. 顺序执行和 并行执行
- 2. 有状态和无状态操作
- 3. 流水线
- 1. 源
- 2. 中间操作
- 3.终结操作
- 5. 总结
1. 什么是函数式编程?
java中的编程都是面向对象的;但是对于一系列计算,用面向对象的手段去解决则会很冗余,针对此,jdk1.8推出了函数式编程,忽略或者说简化对象的含义,更加偏重于结果。
2. 如何用?
在jdk1.8中引入了函数式编程,主要包位于java.util.function
中,包中主要有两个重要的接口
-
Function
该接口只有一个抽象方法,其余的方法都是默认方法。
只接受一个参数R apply(T t)
- BiFunction<T, U, R>
其内部只有一个抽象方法
接受两个参数R apply(T t, U u);
其余的还有一些接口:
- Consumer:接受一个输入,没有输出。抽象方法为 void accept(T t)。
- Supplier:没有输入,一个输出。抽象方法为 T get()。
- Predicate:接受一个输入,输出为 boolean 类型。抽象方法为 boolean test(T t)。
- UnaryOperator:接受一个输入,输出的类型与输入相同,相当于 Function<T, T>。
- BinaryOperator:接受两个类型相同的输入,输出的类型与输入相同,相当于 BiFunction<T,T,T>。
- BiPredicate<T, U>:接受两个输入,输出为 boolean 类型。抽象方法为 boolean test(T t, U u)。
3. Lambda表达式
其实函数式编程做的工作,用面向对象的思维也可以解决,为何java费劲心机的引入函数式编程呢?
可以看一个简单的例子:
下面是简单的new一个线程,用该线程处理打印一行文字。
public class exercise01lambda_no {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World!");
}
}).start();
}
}
但是用lambda表达式主要的内容可以用一行完成,开发者只需要关注最核心的内容即可。
public class exercise02lambda {
public static void main(String[] args) {
new Thread(()-> System.out.println("hello")).start();
}
}
那么下面的代码是如何做到的呢?
只需要lambda中是一个接口类型,并且该接口只有一个抽象的方法。具体的类型,由lambda表达式推断出来,比如上式中,只要该唯一的抽象方法不需要参数,并且该方法没有返回值,就可以匹配上;当然匹配上的可以是Runnable接口或者别的接口中的唯一方法。
箭头的左侧是参数,右侧是具体的逻辑。
4. 流(Stream)
1. 顺序执行和 并行执行
可以将顺序执行理解为电流中的串联;并行执行理解为电流中的并联。后者速度比前者块。
可以使用方法 sequential()
或 parallel()
来将其执行方式设置为顺序或并行
2. 有状态和无状态操作
流操作可以是有状态或无状态的。当一个有状态的操作在处理一个元素时,它可能需要使用处理之前的元素时保留的信息;无状态的操作可以独立处理每个元素:
-
distinct
和sorted
是有状态操作的例子。distinct 操作从流中删除重复元素,它需要记录下之前已经遇到过的元素来确定当前元素是否应该被删除。sorted 操作对流进行排序,它需要知道所有元素来确定当前元素在排序之后的所在位置。
2.filter
和map
是无状态操作的例子。filter 操作在进行过滤时只需要看当前元素即可。map 操作可以独立转换当前元素。一般来说,有状态操作的运行代价要高于无状态操作,因为需要额外的空间保存中间状态信息
3. 流水线
一个流水线由一个源(source),0 到多个中间操作(intermediate operation)和一个终结操作(terminal operation)完成
流的处理流水线在其终结操作运行时才开始执行
- 源:源是流中元素的来源。Java 提供了很多内置的源,包括数组、集合、生成函数和 I/O 通道等。
- 中间操作:中间操作在一个流上进行操作,返回结果是一个新的流。这些操作是延迟执行的。
- 终结操作:终结操作遍历流来产生一个结果或是副作用。在一个流上执行终结操作之后,该流被消费,无法再次被消费
1. 源
一般源都是数组、集合,通过调用stream()
、parallelStream()
方式来创建流
2. 中间操作
中间操作作用在流上,并且返回一个新的流。常见的流式操作:
- map:通过一个 Function 把一个元素类型为 T 的流转换成元素类型为 R 的流。
- flatMap:通过一个 Function 把一个元素类型为 T 的流中的每个元素转换成一个元素类型为 R 的流,再把这些转换之后的流合并。
- filter:过滤流中的元素,只保留满足由 Predicate 所指定的条件的元素。
- distinct:使用 equals 方法来删除流中的重复元素。
- limit:截断流使其最多只包含指定数量的元素。
- skip:返回一个新的流,并跳过原始流中的前 N 个元素。
- sorted:对流进行排序。
- peek:返回的流与原始流相同。当原始流中的元素被消费时,会首先调用 peek 方法中指定的 Consumer 实现对元素进行处理。
- dropWhile:从原始流起始位置开始删除满足指定 Predicate 的元素,直到遇到第一个不满足 Predicate 的元素。
- takeWhile:从原始流起始位置开始保留满足指定 Predicate 的元素,直到遇到第一个不满足 Predicate 的元素。
3.终结操作
常见的终结操作:
1. 遍历
forEach 和 forEachOrdered 遍历流中的每个元素,在使用 forEach 时,并没有确定的处理元素的顺序;forEachOrdered 则按照流的相遇顺序来处理元素。
2. 化简
reduce操作常常用于化简。int reduceNum = Arrays.stream(new int[] {1, 2, 3}).reduce((a,b)->a+b).getAsInt()
主要由以下三部分组成:
- 初始值:在对元素为空的流进行约简操作时,返回值为初始值。
- 叠加器:接受 2 个参数的 BiFunction。第一个参数是当前的约简值,第二个参数是当前元素,返回结果是新的约简值。
- 合并器:对于并行流来说,约简操作可能在流的不同部分上并行执行。合并器用来把部分约简结果合并为最终的结果。
3. 最大值最小值int max = Arrays.stream(new int[] {1, 2, 3}).max().getAsInt();
4. collect
主要由java.util.stream.Collector
提供
collect 会把结果收集到可变的容器中,如 List 或 Set
主要由以下几类:
1. 收集成容器
常见的方法有 toList()、toSet() 和 toMap() 等
2. 对元素进行分组收集
使用 groupingBy 对流中元素进行分组;分组之后的结果是一个 Map,Map 的键是应用 Function 之后的结果,而对应的值是属于该组的所有元素的 List
比如下面的例子对元素的首字母进行分组
final Map<Character, List<String>> names = Stream.of("Alex", "Bob", "David", "Amy")
.collect(Collectors.groupingBy(v -> v.charAt(0)));
System.out.println(names);
- 连接各个元素
把流中的字符串连接起来
String name2 = Stream.of("Alex", "Bob", "David", "Amy").collect(Collectors.joining("-"));
- 将元素分成两类
partitioningBy 操作的作用类似于 groupingBy,只不过分组时使用的是 Predicate,也就是说元素最多分成两组。所得到结果的 Map 的键的类型是 Boolean,而值的类型同样是 List
Map<Boolean, List<String>> names4 = Stream.of("Alex", "Bob", "Amy")
.collect(Collectors.partitioningBy(v -> v.toString().length() > 3));
5. 总结
可以看到,函数式编程对于数据的转化或者是运算效率非常高,可以省略很多代码量,并且具有很强的可读性。