1)函数作为一等公民
将函数作为参数传递给另外一个函数,这是函数式编程的特性之一。
函数可以作为另外一个函数的返回值,也是函数式编程的重要特点。
2)无副作用
函数的副作用指的是函数在调用过程中,除了给出了返回值外,还修改了函数外部的状态。比如,函数在调用过程中,修改了一个全局状态。函数式编程认为,函数的副作用应该尽量避免。如果函数都是显式函数,那么函数的执行显然不会受到外部或者全局信息的影响,因此,对于调试和排错是有益的。(注意:显式函数指函数与外界交换数据的唯一渠道就是参数和返回值,显式函数不会去读取或者修改函数的外部状态。与之相对的是隐式函数,隐式函数除了参数和返回值外,还会读取外部信息,或者可能修改外部信息。)
然而,完全的无副作用实际上做不到的,因为系统总是需要获取或者修改外部信息,同时,模块之间的交互也极有可能是通过共享变量进行的。如果完全禁止副作用的出现,也是一件让人很不愉快的事情。因此,大部分函数式编程语言,如Clojure等,都允许副作用的存在。但是与面向对象相比,这种函数调用的副作用,在函数式编程里,需要进行有效的限制。
3)申明式的(Declarative)
函数式编程是申明式的编程方式。相对于命令式(Imperative)而言,命令式的程序设计喜欢大量使用可变对象和指令。总是习惯于创建对象或者变量,并且修改它们的状态或者值,或者喜欢提供一系列指令,要求程序执行。这种编程习惯在申明式的函数式编程中有所变化。对于申明式的编程范式,不再需要提供明确的指令操作,所有的细节指令将会更好地被程序库所封装,你要做的只是提出你的要求,申明你的用意即可。
下面为一段传统的命令式编程,为了打赢数组中的值,我们需要进行一个循环,并且每次需要判断循环是否结束。在循环体内,要明确地给出需要执行的语句和参数。
public static void imperative() {
int[] iArr = {1,2,3,4,5};
for(int i=0; i<iArr.length; i++) {
System.out.println(iArr[i]);
}
}
与之对应的申明式代码如下:
public static void declarative() {
int[] iArr = {1,2,3,4,5};
Arrays.stream(iArr).forEach(System.out::println);
}
可以看到,变量数组的循环体消失了。pirntln()函数似乎在这里也没有指定任何参数,在此,只是简单地申明了用意。有关循环以及判断循环是否结束等操作都被简单地封装在程序库中。
4)不变的对象
在函数式编程中,几乎所有的传递的对象都不会被轻易修改。
请看如下代码:
static int[] arr={1,2,3,4,5};
Arrays.stream(arr).map((x)->x=x+1).forEach(System.out::pirntln);
System.out.println();
Arrays.stream(arr).forEach(System.out::println);
代码第2行看似对每一个数组成员执行了加1操作。但是在操作完成之后,在最后一行,打印arr数组所有的成员值时,数组成员并没有变化。在使用函数式编程时,这种状态是一种常态,几乎所有的对象都拒绝被修改。这非常类似于不变模式。
5)易于并行
由于对象都处于不变的状态,因此函数式编程更加易于并行。实际上,甚至完全不用担心线程安全问题。这样不仅有利于并行化,同时,并行化后,由于没有同步和锁机制,其性能也会比较好。
6)更少的代码
精简的代码更易于维护。引入函数式编程范式后,可以使用Java用更少的代码完成更多的工作。
如下例子,对于数组中每一个成员,首先判断是否是奇数,如果是奇数,则执行加1,并最终打印数组内所有成员。
数组定义:
static int[] arr={1,2,3,4,5};
传统的处理方式:
for(int i=0; i<arr.length; i++) {
if(arr[i]%2!=0) {
arr[i]++;
}
System.out.println(arr[i]);
}
使用函数式方式:
Arrays.stream(arr).map(x->(x%2==0?x:x+1)).forEach(System.out::println);
可以看到,函数式范式更加紧凑而且简洁。