面试被虐题:说说 JVM 系语言的函数式编程
Java中的函数式编程
函数式编程在上世纪五十年代就有了,只不过在工业界一直不温不火,最近十年才被广泛认知。其理论基础也并非为编程而设计,而是一种数学抽象(Lamda演算),其实初中就学过了,λ表达式。
在 JS(建议把 JS 作为函数式编程思想学习的入门语言,Java 的实现略显臃肿,可能不太便于理解)当中,函数式编程算是应用比较多的了。各现代高级编程语言,都或多或少地支持了函数式编程。
一些基本特点总结:
- 相比平常的指令式编程,函数式编程更在乎执行结果而非过程;
- 函数是一等公民,可以像普通的数值、引用等变量一样赋值、作为参数传递、作为返回值;
- 函数是纯函数,即函数不能产生副作用,如不能修改全局变量等,固定的输入就映射固定的输出。
简单示意一下,不代表任何语言,因为不同语言在实现方式上有差异,但核心思想不变:
1// 定义一个函数g,并赋值给f 2f = g(x, y) = x + y 3// 写一个方法,函数作为参数传递 4printF(g) { 5 print(g(1, 2)) 6} 7// 调用方法 8printF(f) // 打印结果3 9// 作为返回值 10getF() { 11 return f 12}
Java 函数式编程
看了上面的示意,是不是能联想到 Java 的 Runnable 了?
1Runnable f = ()-> { 2 // do something 3} 4void testF(Runnable r, int i) { 5 print(i); 6 r.run(); 7} 8testF(f, 1); 9// 看这个是不是有点函数式编程的影子了,其实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 限制)。
Groovy 函数式编程
Gradle 脚本是基于 Groovy 这门 JVM 动态语言的,用它来表示函数式编程的概念更加清晰:
1def func1 = { msg1 -> 2 println "Look func1 $msg1" 3 4 def fun2 = { msg2 -> 5 println msg2 6 "ret fun2" // 在闭包中最后一行值将直接作为返回结果,加不加return都可 7 } 8 return fun2 9// 等效简化代码 10// return { msg2 -> 11// println msg2 12// "ret fun2" 13// } 14} 15 16println func1('真的')('NB') 17
柯里化理论基础
柯里化是函数式编程的重要特性,简单理解就是把多参函数转化为一个个单一参数的元函数,第一个元函数处理完一个参数后,返回新一个元函数来处理剩下的参数,依此递归,就像工厂的流水线一样工作,各司其职。
我们平时用到的 builder、链式调用,其实都有这种概念在里面。
Java8 柯里化示例:
1import java.util.function.Function; 2import java.util.function.IntFunction; 3import java.util.function.IntUnaryOperator; 4 5public class Curry { 6 private static final Function<Integer, Function<Integer, Function<Integer, Integer>>> CURRYING_1 = 7 x -> y -> z -> (x + y) * z; 8 private static final IntFunction<IntFunction<IntUnaryOperator>> CURRYING_2 = 9 x -> y -> z -> (x + y) * z; 10 private static final TriFunction<Integer, Integer, Integer, Integer> CURRYING_3 = 11 (x, y, z) -> (x + y) * z; 12 13 public static void main(String[] args) { 14 System.out.println(CURRYING_1.apply(1).apply(2).apply(3)); 15 System.out.println(CURRYING_2.apply(1).apply(2).applyAsInt(3)); 16 System.out.println(CURRYING_3.apply(1, 2, 3)); 17 } 18 19 @FunctionalInterface 20 public interface TriFunction<U, T, S, R> { 21 R apply(U u, T t, S s); 22 } 23}
低版本 Java 兼容实践
由于目前大多 Android 项目的 minSDK 对应的 API 等级还是 19 或者 23,且未升级至 Studio 4.0,并不能直接使用 Java 8 的全部特性,因此只能在编码层面进行部分特性的兼容:
1// build.gradle 2android { 3 compileOptions { 4 sourceCompatibility JavaVersion.VERSION_1_8 5 targetCompatibility JavaVersion.VERSION_1_8 6 } 7}
不过,我们也可以自己复制 java.util.function 包中的代码来实现函数式编程(比如 AndroidX 的工具包中就单独实现了 Consumer 接口),具体可参考 androidx.core.util.Consumer 的相关引用。
对函数式编程支持程度高低的一个重要特征是函数是否作为编程语言的一等公民出现,也就是编程语言是否有内置的结构来表示函数。作为面向对象的编程语言,Java 中使用接口来表示函数。
1// 比如Consumer就是一种只接受一个输入,而没有输出的特殊函数 2public interface Consumer<T> { 3 void accept(T t); 4} 5 6// 为通知构建,创建一个PendingIntent 7public static PendingIntent createActivityI(int type, Consumer<Intent> consumer) { 8 Context appCtx = MyApp.getContext(); // 获取App全局Context 9 Intent intent = new Intent(); 10 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 11 if (consumer != null) { 12 consumer.accept(intent); 13 } 14 return PendingIntent.getActivity(appCtx, type, intent, PendingIntent.FLAG_UPDATE_CURRENT); 15} 16// 外部调用 17PendingIntent clickI = createActivityI(INTENT_TYPE_TEST, intent -> { 18 intent.setData(xxx); 19 intent.putExtra(xxx); 20 // ...对intent对象各种操作,无需关心内部是如何初始化的(实例化),我们只是外部消费者(consumer) 21});
上面这段代码可能咋一看跟真正的函数式编程并没有什么卵关系,甚至一般的 builder 模式也能实现。
但我们应该把 intent -> { ... } 看成一个λ函数表达式,intent 是唯一参数且不可变,并且我们应当遵守纯函数的规范,即 { ... } 函数实现内部只对 intent 进行修饰等操作,不应该去做其他无关的事情(比如修改外部变量,甚至是调起其他功能模块等)。
在消费者 consumer.accept() 的瞬间,内外互不相知干了什么,天然地做到了业务逻辑隔离。
请勿滥用
越抽象和高级的东西,内部消耗越大,乃自然之理。虽然函数式编程有很多优点,如可读性好,函数无副作用,参数不可变(理论上适合并行操作,不用考虑死锁,实际上性能不够,是不是挺矛盾的?)等。
但相比指令式编程,大量使用函数式编程,会影响程序性能。不适合做IO密集型操作和一些高性能的UI操作。从Java函数式编程的实现来看,内部也涉及到比较多的函数递归嵌套,给栈区带来一定的压力。
合理使用:平时工作中可以利用函数式编程的理念来简化业务代码,如上文示例,还是蛮好的。