一文读懂Java函数式编程
函数式编程中没有赋值语句,因此变量一旦有了值就不会再改变。更通俗的讲,函数式编程不修改变量,这样消除了bug的一个主要来源,也使得执行顺序变得无关紧要。这意味着状态不能保存在变量中——即程序是“引用透明”的。函数式编程使用参数保存状态,避免了使用程序状态和可变对象,降低了程序复杂度,而这也正是函数式编程的精髓。函数式编程强调执行的结果,而非执行的过程。我们先构建一系列简单却具有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算,这是函数式编程的基本思想。
1. 什么是函数式编程Functional Programming
我们查看wikipedia上的相关定义,我们可以总结出以下几点
1.1 函数是"第一等公民"
指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
1.2 闭包和高阶函数
闭包是起函数的作用并可以像对象一样操作的对象。与此类似,函数式编程语言支持高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在大多数情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用函数式编程的最大好处
1.3 不改变状态(引用透明)
函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。在其他类型的语言中,变量往往用来保存"状态"(state)
什么是引用透明?
如果一个函数只会受到入参的变化,那么这个函数每次的调用都会是相同的
一个函数f(x),里面调用了g(x),g(x)里面又调用了h(x),h(x)最终计算出了结果,作为f(x)的返回值返回了。如果所有的状态都没有改变,f(x)下一次再调用相同的参数的时候,应该会得到完全一样的结果,那这个时候其实不用再调用g(x)和h(x)了,也可以得到完全一样的结果。当一个函数,不依赖“外部”变量和状态,只依赖入参的变化而影响函数最终返回值,也就是说入参相同,得到的返回值结果一定相同,如果函数具有这种性质,就可以说这个函数是引用透明的。
1.4 递归(使用参数保存状态,而非变量)
函数式编程是用递归做为控制流程的机制。
1.5 只用"表达式",不用"语句",没有副作用
“表达式”(expression)是一个单纯的运算过程,总是有返回值;“语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。“语句"属于读写操作,所以就被排斥在外。函数式编程强调没有"副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
1.5.1 什么是副作用
在stackoverflow中有一个提问Side Effects in Functional Programming
在函数式编程中,如下的行为是称之为副作用的:
- 修改一个变量
- 修改一个对象的字段值
- 抛出异常
- 在控制台显示信息、从控制台接收输入
- 在屏幕上显示(GUI)
- 读写文件、网络、数据库。
如果函数式编程中,我们没有任何输入、输出等有副作用的操作,能编写什么样的程序呢?如果能,又是怎么做到的?
函数式编程是一种隔离应用逻辑(表达)与实际运行时解释的方法。你的“函数式代码”用来表达(但不执行)所需要达成的执行效果,返回某种形式的数据结构来描述这个计算结果。然后,我们会在一个解释器中来执行该结果,后者不是函数式的。
完全的纯函数式编程是不可能的,一段纯函数式代码,除了会让CPU产生热量,不会产生其他副作用,我们还需要一个解释器来真实的进行IO操作,如读写文件、网络等。将两者隔离会带来很多的优势:纯函数式代码,更易于测试,引用透明性会让代码可读性提升,提高开发阶段的效率,减少BUG,提高产品质量。同时,函数式编程,相比命令式编程,可以让很多复杂的代码便得更简单。比如:Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!
因此,**在函数式语言中,处理副作用的方式就是隔离副作用,使用纯函数式的代码,来计算”副作用“,并将其表示为一个”值“,然后将该值交给一个解释器来执行 **
2. Java中的函数接口
2.1. Function
Function是从T到R的一元映射函数。将参数T传递给一个函数,返回R。即R = Function(T)
public static <T,U> U change(T t, Function<T,U> f, Class<U> uClass) {
U u = f.apply(t);
return u;
}
Integer change = change("6789", Integer::parseInt, Integer.class);
System.out.println(change); // 6789
List change = change("11", (t) -> {
List list = new LinkedList();
int i = Integer.parseInt(t);
list.add(i*i);
return list;
}, List.class);
System.out.println(change.toString()); // [121]
2.2. Predicate
Predicate是一个谓词函数,主要作为一个谓词演算推导真假值存在,返回布尔值的函数。Predicate等价于一个Function的boolean型返回值的子集。
predicate最常用的莫过于 Stream filter(Predicate<? super T> predicate);
public static <T> boolean checkString(T s, Predicate<T> p){
return p.test(s);
}
boolean b = checkString("testse", (t) -> t.length() > 5);
System.out.println(b); // true
boolean c = checkString(987654321, (t) -> t < 5);
System.out.println(c); // false
2.3. Consumer
Consumer是从T到void的一元函数,接受一个入参但不返回任何结果的操作。
Consumer最常用的肯定是 default void forEach(Consumer<? super T> action) {}
public static <T> void consumerTest(T input, Consumer<T> consumer) {
consumer.accept(input);
}
consumerTest("Consumer test string", n -> System.out.println(n + " add suffix"));
// Consumer test string add suffix
2.4. Supplier
java.util.function.Supplier 接口仅包含一个无参的方法:T get(),用来获取一个泛型参数指定类型的对象数据
该接口被称为生产型接口,指定接口的泛型是什么类型,那么get方法就会产生什么类型的数据
public static <T> T supplierTest(Supplier<T> supplier) {
return supplier.get();
}
System.out.println(supplierTest(() -> 1024 >> 2)); // 256
2.5. BiFunction
java.util.function.BiFunction<T, U, R>接收两个参数返回一个结果,如果接收参数和返回参数的类型相同,就可用java.util.function.BinaryOperator替换。
private static <T,P> void biFunctionTest(T container, P param, BiFunction<T,P,T> function) {
function.apply(container, param);
}
Map<String, String> map = new HashMap<>();
biFunctionTest(map, "test_param", (t, p) -> {
t.put("url", "http://" + p);
return t;
});
biFunctionTest(map, 6666666, (t, p) -> {
t.put("number", String.valueOf(p));
return t;
});
System.out.println(map);
// {number=6666666, url=http://test_param}