Java中的函数式编程

闲聊

函数式编程在上世纪五十年代就有了,只不过在工业界一直不温不火,最近十年才被广泛认知。其理论基础也并非为编程而设计,而是一种数学抽象(Lamda演算),其实初中就学过了,λ表达式。

在JS(建议把JS作为函数式编程思想学习的入门语言,Java的实现略显臃肿,可能不太便于理解)当中,函数式编程算是应用比较多的了。各现代高级编程语言,都或多或少地支持了函数式编程。

一些基本特点总结

  • 相比平常的指令式编程,函数式编程更在乎执行结果而非过程;
  • 函数是一等公民,可以像普通的数值、引用等变量一样赋值、作为参数传递、作为返回值;
  • 函数是纯函数,即函数不能产生副作用,如不能修改全局变量等,固定的输入就映射固定的输出。

简单示意一下

不代表任何语言,因为不同语言在实现方式上有差异,但核心思想不变:

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个函数g,并赋值给f
f = g(x, y) = x + y
// 写一个方法,函数作为参数传递
printF(g) {
print(g(1, 2))
}
// 调用方法
printF(f) // 打印结果3
// 作为返回值
getF() {
return f
}

Java函数式编程

看了上面的示意,是不是能联想到Java的Runnable了?

1
2
3
4
5
6
7
8
9
Runnable f = ()-> {
// do something
}
void testF(Runnable r, int i) {
print(i);
r.run();
}
testF(f, 1);
// 看这个是不是有点函数式编程的影子了,其实Runnable接口的设计在Java 8之前就有了,还是很有远见的,在此可以把f看成一个无参无返回值的函数,也算是低配版的函数式编程嘛~

所以我们在Java 8的编程环境下,经常看到IDE提示new Runnable……可以转化成lamda表达式。

真正的函数式编程本来Java 7就会支持的,但是甲骨文跳票你懂的,于是functional programming在Java 8才正式推出。从 java.util.function 包即可管中窥豹。

Java后端开发中早就用烂了,在Android开发中必须API大于等于24才能完全开启Java 8特性(最新:Studio 4.0推出的新版Gradle插件已经支持解糖,不再需要API限制:Java 8+ API desugaring support (Android Gradle Plugin 4.0.0+))。

Groovy函数式编程

Gradle脚本是基于Groovy这门JVM动态语言的,用它来表示函数式编程的概念更加清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def func1 = { msg1 ->
println "Look func1 $msg1"

def fun2 = { msg2 ->
println msg2
"ret fun2" // 在闭包中最后一行值将直接作为返回结果,加不加return都可
}
return fun2
// 等效简化代码
// return { msg2 ->
// println msg2
// "ret fun2"
// }
}

println func1('真的')('NB')

在这里插入图片描述

柯里化理论基础

柯里化是函数式编程的重要特性,简单理解就是把多参函数转化为一个个单一参数的元函数,第一个元函数处理完一个参数后,返回新一个元函数来处理剩下的参数,依此递归,就像工厂的流水线一样工作,各司其职。

我们平时用到的builder、链式调用,其实都有这种概念在里面。
具体原理可以参考资料,还是蛮有意思的:

Java8柯里化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

public class Curry {
private static final Function<Integer, Function<Integer, Function<Integer, Integer>>> CURRYING_1 =
x -> y -> z -> (x + y) * z;
private static final IntFunction<IntFunction<IntUnaryOperator>> CURRYING_2 =
x -> y -> z -> (x + y) * z;
private static final TriFunction<Integer, Integer, Integer, Integer> CURRYING_3 =
(x, y, z) -> (x + y) * z;

public static void main(String[] args) {
System.out.println(CURRYING_1.apply(1).apply(2).apply(3));
System.out.println(CURRYING_2.apply(1).apply(2).applyAsInt(3));
System.out.println(CURRYING_3.apply(1, 2, 3));
}

@FunctionalInterface
public interface TriFunction<U, T, S, R> {
R apply(U u, T t, S s);
}
}

低版本Java兼容实践

由于目前大多Android项目的minSDK对应的API等级还是19或者23,且未升级至Studio 4.0,并不能直接使用Java 8的全部特性,因此只能在编码层面进行部分特性的兼容:

1
2
3
4
5
6
7
// build.gradle
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

不过,我们也可以自己复制 java.util.function 包中的代码来实现函数式编程(比如AndroidX的工具包中就单独实现了Consumer接口),具体可参考 androidx.core.util.Consumer 的相关引用。

对函数式编程支持程度高低的一个重要特征是函数是否作为编程语言的一等公民出现,也就是编程语言是否有内置的结构来表示函数。作为面向对象的编程语言,Java 中使用接口来表示函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 比如Consumer就是一种只接受一个输入,而没有输出的特殊函数
public interface Consumer<T> {
void accept(T t);
}

// 为通知构建,创建一个PendingIntent
public static PendingIntent createActivityI(int type, Consumer<Intent> consumer) {
Context appCtx = MyApp.getContext(); // 获取App全局Context
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (consumer != null) {
consumer.accept(intent);
}
return PendingIntent.getActivity(appCtx, type, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
// 外部调用
PendingIntent clickI = createActivityI(INTENT_TYPE_TEST, intent -> {
intent.setData(xxx);
intent.putExtra(xxx);
// ...对intent对象各种操作,无需关心内部是如何初始化的(实例化),我们只是外部消费者(consumer)
});

上面这段代码可能咋一看跟真正的函数式编程并没有什么卵关系,甚至一般的builder模式也能实现。

但我们应该把 intent -> { … } 看成一个λ函数表达式,intent是唯一参数且不可变,并且我们应当遵守纯函数的规范,即 { … } 函数实现内部只对 intent 进行修饰等操作,不应该去做其他无关的事情(比如修改外部变量,甚至是调起其他功能模块等)。

在消费者 consumer.accept() 的瞬间,内外互不相知干了什么,天然地做到了业务逻辑隔离。

参考

请勿滥用

越抽象和高级的东西,内部消耗越大,乃自然之理。虽然函数式编程有很多优点,如可读性好,函数无副作用,参数不可变(理论上适合并行操作,不用考虑死锁,实际上性能不够,是不是挺矛盾的?)等。

但相比指令式编程,大量使用函数式编程,会影响程序性能。不适合做IO密集型操作和一些高性能的UI操作。从Java函数式编程的实现来看,内部也涉及到比较多的函数递归嵌套,给栈区带来一定的压力。

合理使用:

平时工作中可以利用函数式编程的理念来简化业务代码,如上文示例,还是蛮好的。