浅谈JVM语言之函数式编程
Java中的函数式编程
闲聊
函数式编程在上世纪五十年代就有了,只不过在工业界一直不温不火,最近十年才被广泛认知。其理论基础也并非为编程而设计,而是一种数学抽象(Lamda演算),其实初中就学过了,λ表达式。
在JS(建议把JS作为函数式编程思想学习的入门语言,Java的实现略显臃肿,可能不太便于理解)当中,函数式编程算是应用比较多的了。各现代高级编程语言,都或多或少地支持了函数式编程。
一些基本特点总结
- 相比平常的指令式编程,函数式编程更在乎执行结果而非过程;
- 函数是一等公民,可以像普通的数值、引用等变量一样赋值、作为参数传递、作为返回值;
- 函数是纯函数,即函数不能产生副作用,如不能修改全局变量等,固定的输入就映射固定的输出。
简单示意一下
不代表任何语言,因为不同语言在实现方式上有差异,但核心思想不变:
1 | // 定义一个函数g,并赋值给f |
Java函数式编程
看了上面的示意,是不是能联想到Java的Runnable了?
1 | Runnable 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 | def func1 = { msg1 -> |
柯里化理论基础
柯里化是函数式编程的重要特性,简单理解就是把多参函数转化为一个个单一参数的元函数,第一个元函数处理完一个参数后,返回新一个元函数来处理剩下的参数,依此递归,就像工厂的流水线一样工作,各司其职。
我们平时用到的builder、链式调用,其实都有这种概念在里面。
具体原理可以参考资料,还是蛮有意思的:
Java8柯里化示例:
1 | import java.util.function.Function; |
低版本Java兼容实践
由于目前大多Android项目的minSDK对应的API等级还是19或者23,且未升级至Studio 4.0,并不能直接使用Java 8的全部特性,因此只能在编码层面进行部分特性的兼容:
1 | // build.gradle |
不过,我们也可以自己复制 java.util.function
包中的代码来实现函数式编程(比如AndroidX的工具包中就单独实现了Consumer接口),具体可参考 androidx.core.util.Consumer
的相关引用。
对函数式编程支持程度高低的一个重要特征是函数是否作为编程语言的一等公民出现,也就是编程语言是否有内置的结构来表示函数。作为面向对象的编程语言,Java 中使用接口来表示函数。
1 | // 比如Consumer就是一种只接受一个输入,而没有输出的特殊函数 |
上面这段代码可能咋一看跟真正的函数式编程并没有什么卵关系,甚至一般的builder模式也能实现。
但我们应该把 intent -> { … } 看成一个λ函数表达式,intent是唯一参数且不可变,并且我们应当遵守纯函数的规范,即 { … } 函数实现内部只对 intent 进行修饰等操作,不应该去做其他无关的事情(比如修改外部变量,甚至是调起其他功能模块等)。
在消费者 consumer.accept()
的瞬间,内外互不相知干了什么,天然地做到了业务逻辑隔离。
参考
请勿滥用
越抽象和高级的东西,内部消耗越大,乃自然之理。虽然函数式编程有很多优点,如可读性好,函数无副作用,参数不可变(理论上适合并行操作,不用考虑死锁,实际上性能不够,是不是挺矛盾的?)等。
但相比指令式编程,大量使用函数式编程,会影响程序性能。不适合做IO密集型操作和一些高性能的UI操作。从Java函数式编程的实现来看,内部也涉及到比较多的函数递归嵌套,给栈区带来一定的压力。
合理使用:
平时工作中可以利用函数式编程的理念来简化业务代码,如上文示例,还是蛮好的。