函数式编程学习
1. 介绍
1.1 概念
- 函数式编程是一种抽象度很高的「编程范式」,属于「结构化编程」的一种,除它之外,还有命令式编程,声明式编程;
- 函数式编程像是“流水线”,允许我们将数据和处理逻辑封装到函数中,将函数作为基本运算单元,并且将这个函数本身作为参数传入另外一个函数的同时还返回一个函数(balalala);
- 函数式编程的基础是 lambda 计算,它的关注点是运算过程,也就是对数据做了什么操作;
- 我们经常把支持函数式编程的编码风格称为 Lambda 表达式,Java平台从 Java 8 开始,引入了 lambda 表达式 和 Stream API。
1.2 特点
闭包和高阶函数:函数式编程支持将函数作为第一类对象(函数本身与其他数据类型一样,可以赋值给其他变量,也可以将其作为参数传递给其他函数),在某些情况下甚至允许返回一个函数作为其参数;
惰性计算:也叫惰性求值、延迟求值,是一种软件设计和架构设计思想,核心:少做无用功,等真正需要的时候才计算,节约内存开支,提升性能;
递归:函数式编程用用递归做为控制流程的机制,递归算法是一种典型的函数式编程案例;
举个栗子:已知数列 1、1、2、3、5、8…,求第 n 项的值
规律:
代码:
1
2
3
4
5public static int getResult(int num){
//边界判断
if (num<=2) return 1;
return getResult(num - 1) + getResult(num - 2);
}这个 f(n) 就是递归函数
来源:https://blog.csdn.net/haohaounique/article/details/117573671
只用「表达式」,不用「语句」:函数式编程要求函数必须有返回值;
没有「副作用」:函数式编程强调没有”副作用”,变量值一旦被指派就永远不会改变,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值;
引用透明性:如果提供同样的输入,函数总是返回同样的结果;
来源:百度百科、廖雪峰的官方网站
2. Lambda 表达式
Lambda 表达式 是 JDK8 中的语法糖。使用 Lambda 表达式可以对某些匿名内部类的写法进行简化。它是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是关注对数据进行了什么操作。
2.1 基本格式
1 | (参数列表)->{代码} |
例:
1 | new Thread(new Runnable() { |
使用 Lambda 表达式
1 | new Thread(() -> { |
Idea 中光标移动到匿名内部类,按下 Alt+Enter,只要可以简化为 Lambda 表达式就会出现Replace with lamdbda
转回普通写法
2.2 省略规则
- 参数类型可省略(可通过上下文推断出参数类型);
- 方法体只有一句代码时,大括号、return、结尾分号可省略;
- 方法只有一个参数时可以省略。
lambda 表达式只能引用标记了 final 的外层局部变量,不能在 lambda 表达式内部修改定义在外部的局部变量,否则编译错误
3. Stream 流
3.1 概述
JDK8 中的 Stream API 不同于 java.io 包中的 InputStream 和 OutputStream,它使用的是函数式的编程方式,对数据源(可以来自集合,数组,I/O channel, 产生器 generator等)进行链状流式操作;我们可以把我们的需要处理的集合或数组看为「原材料」,把这个「原材料」放在 Stream API 提供的各种操作(如:filter、sorted、map、……)对其进行加工,最后得到我们想要的产品。
3.2 创建流
3.2.1 stream() 串行流
stream() 是线程安全的,数据量少且业务简单用串行
单列集合:
1 | list.stream(); |
双列集合:先转换为单列集合
1 | Map<String,String> map = new HashMap<>(); |
数组:
1 | Integer[] arr = {1,2,3,4,5}; |
3.2.2 parallelStream() 并行流
线程不安全,parallelStream() 底层使用Fork/Join框架实现,是多线程异步任务的一种实现;数据量大,且业务复杂,用并行
使用方式:将上面的stream()
换为parallelStream()
即可
1 | list.parallelStream() |
3.3 中间操作
中间操作都会返回流对象本身,这样多个操作可以串联成一个管道, 如同流式风格。
map
将一种操作映射到 Stream 中的每一个元素上,可自定义处理的返回值,最后返回一个新的 Stream。
1 | Integer[] integers = new Integer[]{1, 23, 4, 5, 6, 7}; |
filter
对一个 Stream 的所有元素一一进行测试,不满足条件的就被“过滤掉”了,剩下的满足条件的元素就构成了一个新的 Stream。
1 | List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); |
distinct
去除 Stream 中的相同元素(该方法依赖于 equals 方法)。
1 | strings.stream().distinct() |
sorted
对 Stream 中元素进行排序,传入空参时,Stream 中的元素必须实现 Comparable 接口(也就是实现 compareTo 方法,告诉程序如何排序)。
1 | integers.stream().sorted() |
如果传入的是自定义对象,该对象必须实现 Comparable 接口,否则抛出ClassCastException
异常。
1 | users.stream().sorted() |
1 | public class User implements Comparable<User> { |
又或者这样
1 | List<User> users = new ArrayList(); |
skip
跳过 Stream 中的前 n 个元素,返回剩下元素组成的新的 Stream。
limit
设置 Stream 的最大元素个数,超出部份会被抛弃。
flatMap
map 只能把一个对象转变为另一个对象来作为新的元素,而 flatMap 可以将一个对象转换为多个对象作为 Stream 中的元素。
1 | Stream<List<Integer>> s1 = Stream.of( |
parallel
返回并行的等效 Stream。可能会返回自身
1 | Arrays.stream(integers).parallel().forEach(System.out::println); |
3.4 终结操作
惰性求值,如果 Stream 没有终结操作,那么我们对 Stream 做的中间操作也不会执行,经过终结操作之后的 Stream 就不能再被使用。
如:
1 | users.stream().sorted(); //没有终结操作 |
forEach
遍历 Stream 中的每个元素。
1 | List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); |
count
返回 Stream 中元素的个数
min&max
返回 Stream 中元素的最值,返回值为 Optional 对象,与 sorted 类似,需要指定比较规则。
collect
把 Stream 换成一个集合(List、Set、Map)
1 | strings.stream().distinct().collect(Collectors.toList())// toSet(),toMap() |
anyMatch
判断 Stream 中是否有匹配条件的元素,返回 boolean 值。
1 | //判断流中的用户是否存在年龄大于 20 的 |
allMatch
判断所有的用户是否都是成年人。
1 | boolean b = users.stream().allMatch(user -> user.getAge() >= 18) |
noneMatch
判断 Stream 中的元素是否都不符合条件。
findAny
获取 Stream 中的任意一个元素。
findFirst
获取 Stream 中的第一个元素。
reduce
把 Stream 中的元素组合起来,我们可以传入一个初始值,并指定计算方式,它会按照这个计算方式从 Stream 中取出元素与初始值进行计算,计算的结果作为参数再与 Stream 中取出的元素进行后面的计算,通过这样指定方式累积计算的过程得出结果。
1 | T result = identity; |
举个栗子:
1 | Integer[] integers = new Integer[]{1, 2, 3, 4, 5, 6}; |
单个参数的模式(不用指定初始值)
1 | Optional<Integer> min = Arrays.stream(integers).reduce(Integer::min); |
4. Optional
4.1 概述
很多情况下代码容易出现空指针异常,尤其对象的属性是另外一个对象的时候,这种情况下Java 8 引入了optional来避免空指针异常。
4.2 使用
4.2.1 创建 Optional 对象
Optional 就像是包装类,可以把我们的具体数据封装 Optional 对象内部, 然后我们去使用它内部封装好的方法操作封装进去的数据就可以很好的避免空指针异常。
推荐使用Optional.ofNullable
来把数据封装成一个optional对象。
1 | Author author = getAuthor(); |
如果确定一个对象不是空的话就可以用Optional.of
这个静态方法来把数据封装成 Optional 对象(如果传入为空还是会报空指针异常)。
1 | Optional<Author> author = Optional.of(author); |
如果我们确定一个方法返回为 null,那么可以使用Optional.empty()
来封装这个结果。
1 | //代表这个值为 null |
4.2.2 安全消费值
当我们获取到一个Optional对象的时候,可以用ifPresent方法来去消费其中的值, 这个方法会先去判断是否为空,不为空才会去执行消费代码,优雅避免空指针 OptionalObject.ifPresent()
。
1 | Optional<Author> authorOptional = Optional.of(author); |
4.2.3 安全获取值
可以使用 Optional 对象的 get 方法获取值,但如果 Optional 封装的数据为空时还是会发生异常。所以推荐使用以下方法。
orElseGet
获取数据并且设置数据为空时的默认值
1
2//若为空,返回一个默认对象
Author author = authorOptional.orElseGet(()->new Author())orElseThrow
如果 Optional 封装的数据为空,抛出一个自定义的异常(统一异常处理)
4.2.4 过滤
我们可以使用 Optional 的filter()
方法对数据进行过滤,如果原来是有数据的,但是不符合判断,也会变成一个无数据的 Optional 对象。
1 | userOptional.filter(u -> u.getAge() < 0).orElseThrow(()->new RuntimeException("数据为空")); |
4.2.5 判断
可以通过 Optionald的isPresent()
判断数据是否存在,存在为 true,否则 false。
4.2.6 数据转换
Optional 还提供map()
方法对数据进行转换,转换得到的数据还是 Optional 包装好的,保证安全使用。
1 | userOptional.map(user -> user.getPets()) |
5. 函数式接口
5.1 概述
只有一个抽象方法的接口就是函数式接口,JDK8 的函数式接口都加上了 @FunctionalInterface 注解进行标识,但是无论加不加该注解,只要接口中只有一个抽象方法,都是函数式接口。
1 | java.util.function |
5.2 常见函数式接口
Consumer 消费接口
可以对传入的参数进行消费操作。
1 |
|
Function 计算转换接口
对传入的参数计算或转换,把结果返回
1 |
|
Predicate 判断接口
对传入的参数条件进行判断,返回判断结果。
1 |
|
Supplier 生产接口
根据其处理的泛型创建对应的对象并返回。
1 |
|
5.3 常用的默认方法
1 | public interface Predicate<T> { |
举个栗子:
在使用 Predicate 接口的时候可能需要进行判断条件的拼接,而 and 方法相当于使用 && 来拼接两个判断条件。
1 | authors.getAuthors().stream().filter(new Predicate<Author>({ |
6. 方法引用
我们在使用 lambda 时,如果方法体中只有一个方法的时候,包括构造方法,可以用方法引用进一步简化代码。
6.1 基本格式
1 | 类名或对象名::方法名 |
6.2 引用规则
引用类静态方法;
1
类名::方法名
如果我们在重写方法的时候,方法体中只有一行代码, 并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有参数都按照顺序传入了这个静态方法中, 这个时候我们就可以引用类的静态方法。
引用对象的实例方法;
1
对象名::方法名
使用前提:如果我们在重写方法的时候,方法体只有一行代码,并且这行代码是调用了某个对象的成员方法, 并且我们把要重写的抽象方法里面中所有的参数都按照顺序传入了这个成员方法(就是类的方法)中,这个时候我们就可以引用对象的实例方法。
引用类的实例方法
1
类名::方法名
使用前提:如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法, 并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。
构造器引用
1
类名::new
7. 其他
基本数据类型优化:很多 Stream 方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型,即使我们操作的是 整数小数,实际使用还是他们的包装类,JDK5 中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便, 但是你一定要知道装箱拆箱也是需要一定的时间的,虽然这个时间消耗很小,但是在大量数据的不断重复的情况下,就不能忽视这个时间损耗了, Stream 对这块内容进行了优化,提供很多针对基本数据类型的方法。 例如:mapToInt、mapToLong、mapToDouble、flatMapToInt…
比如前面我们用的 map(),返回的是 Stream,如果用 mapToInt(),最后返回的就是 int 值。