函数式编程

在Java的世界里,第一要素是“类”,而在函数式编程里,第一要素则是“函数”。

何为函数式编程

函数式编程是一种编程范式(programming paradigm),也就是如何编写程序的方法论。
它属于“结构化编程”的一种,主要思想是想把运算过程尽量写成一系列嵌套的函数调用。
举例来说,现在有这样一个数学表达式:

1
(1+2) * 3 - 4

传统的过程式编写,可能这样写:

1
2
3
var a = 1 + 2;
var b = a * 3;
var c = b - 4;

函数式编程要求使用函数,可以把这一过程定义为不同的函数,然后写成下面这样:

1
var result = subtract(multiply(add(1,2), 3), 4);

这就是函数式编程

特点

  1. 函数是第一等公民
    函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

    1
    2
    var print = function(i){ console.log(i);};
    [1,2,3].forEach(print);
  2. 只用“表达式”,不用“语句”
    “表达式”(expression)是一个单纯的运算过程,总是有返回值;
    “语句”(statement)是执行某种操作,没有返回值。
    函数式编程要求,只使用表达式,不使用语句。
    也就是说,每一步都是单纯的运算,而且都有返回值。

  3. 没有“副作用”
    “副作用”是指,函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
    函数式编程强调没有“副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
  4. 不修改状态
    在其他类型的语言中,变量往往用来保存“状态”(state)。
    不修改变量,意味着状态不能保存在变量中。
    函数式编程使用参数保存状态,最好的例子就是递归。
  5. 引用透明
    引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或“状态”,
    只依赖于输入的参数,任何时候只要参数相同,引用函数所得的返回值总是相同的。

意义

  1. 代码简洁,开发快速
  2. 接近自然语言,易于理解
  3. 更方便的代码管理
    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。
    因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和除错,以及模块化组合。
  4. 易于“并发”
    函数式编程不需要考虑“死锁”,因为它不修改变量,所以根本不存在“锁”线程的问题。
    1
    2
    3
    var s1 = Op1();
    var s2 = Op2();
    var s3 = concat(s1, s2);

由于s1和s2互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分别放在两个线程上完成。

  1. 代码的热升级
    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。
    所以,可以在运行状态下直接升级代码不需要重启。



    扫盲

    函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
    在函数式编程中,我们要做的就是把函数传来穿去,而这个,说成术语,我们把他叫做高阶函数。
    f(x) = y ,那么这个函数无论在什么场景下,都会得到同样的结果,这个我们称之为函数的确定性。
    函数式编程的抽象本质则是将函数也作为一个抽象单位,而反映成代码形式,则是高阶函数。
    循环是在描述我们该如何地去解决问题。
    递归是在描述这个问题的定义。
    其实尾递归就是不要保持当前递归函数的状态,而把需要保持的东西全部用参数给传到下一个函数里,这样就可以自动清空本次调用的栈空间。

    Java8 Lambda表达式

    匿名内部类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class CommandTest
    {

    public static void main(String[] args){
    ProcessArray pa = new ProcessArray();
    int[] target = {3, -4, 6, 4};
    //处理数组,具体处理行为取决于匿名内部类
    pa.process(target, new Command(){
    public void process(int[] target){
    int sum = 0;
    for(int tmp: target){
    sum += tmp;
    }
    System.out.println("数组元素的总和:" + sum);
    }
    });
    }
    }

Lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public  class CommandTest2
{

public static void main(String[] args){
ProcessArray pa = new ProcessArray();
int[] array = [3, -4, 6, 4];
//处理数组,具体处理行为取决于匿名内部类
pa.process(array, (int[] target)->{
int sum = 0;
for(int tmp : target){
sum += tmp;
}
System.out.println("数组元素的总和:" + sum);
});
}
}
Lambda表达式的主要作用就是代替匿名内部类的繁琐语法。
它由三部分组成:
形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
箭头(->)必须通过英文中画线号和大于符号组成。
代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。
Lambda代码块只有一条return语句,甚至可以省略return关键字。
```java
interface Eatable
{

void taste();
}

interface Flyable
{

void fly(String weather);
}

interface Addable
{

int add(int a, int b);
}

public class LambdaQs{
//调用该方法需要Eatable对象
public void eat(Eatable e){
System.out.println(e);
e.taste();
}

//调用该方法需要Flyable对象
public void drive(Flyable f){
System.out.println("我正在驾驶:" + f);
f.fly("碧空如洗的晴日");
}

//调用该方法需要Addable对象
public void test(Addable add){
System.out.println("5与3的和为: " + add.add(5, 3));
}

public static void main(String[] args){
LambdaQs lq = new LambdaQs();
//Lambda表达式的代码只有一条语句,可以省略花括号
lq.eat(()->System.out.println("苹果的味道不错!"));
//Lambda表达式的形参列表只有一个形参,可以省略圆括号
lq.drive(weather->{
System.out.println("今天天气是:" + weather);
System.out.println("直升飞机飞行平稳");
});
//Lambda表达式的代码块只有一条语句,可以省略花括号
//代码块只有一条语句,即使该表达式需要返回值,也可以省略return关键字
lq.test((a, b)->a + b);
}
}

为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以由如下三种常见的方法:

  • 将Lambda表达式赋值给函数式接口类型的变量
  • 将Lambda表达式作为函数式接口类型的参数传给某个方法
  • 使用函数式接口对Lambda表达式进行强制类型转换