Java 函数式编程基础

函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称 Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为 Lambda 计算。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

虽然Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数。从 Java 8 开始,Java 平台支持函数式编程。

将一个实例传入一个方法中,实例的成员方法就相当于作为参数传入其他函数的函数,这样就可以实现函数式编程。

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
// 定义接口
interface IPrint {
void print();
}

// 定义接口的实现子类
class Print implements IPrint {
public void print() {
// 覆写IPrint中的print()方法。
System.out.println("实现子类的传入信息");
}
}

public class Lambda {

public static void main(String[] args) {
// 传入子类的实例化对象
fun(new Print());
}

public static void fun(IPrint msg) { // 接收接口对象
msg.print();
}

}

只需要编写IPrint接口的不同实现子类,设置其中的成员方法内容,就可以在不动方法fun(IPrint msg)本身的前提下改变传入到方法的“函数”,进而改变方法的执行结果,实现传入普通参数难以达到的灵活性。比如:

1
2
3
4
5
6
7
class Print implements IPrint {
public void print() {
// 覆写IPrint中的print()方法。
System.out.println("打印普通信息");
System.err.println("打印错误信息");
}
}

同时打印普通信息和错误信息,甚至做出更复杂的效果,这是只接收字符串等参数传入的方法无法灵活做到的。以往想要改变一个方法的逻辑,必须要改写方法本身,现在只需要改变传入的“函数”即可。

匿名内部类

如果Print类只使用一次,还要将其定义为一个具体的类,就很麻烦。这时就可以用到匿名内部类了。

内部类是指在类内部定义的类结构,利用内部类可以方便地实现私有属性的互相访问,一般的内部类需要明确地使用class进行定义,而匿名内部类较为特殊,是没有名字的类。必须在抽象类或接口的基础上才能定义,可以实例化抽象类或接口,适合用来创建一次性子类

语法:new 抽象类名或接口名() {},大括号内部的内容即是该实例化子类的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface IPrint {
void print();
}

public class Lambda {

public static void main(String[] args) {
//匿名内部类
fun(new IPrint() { // 实例化接口IPrint
//覆写IPrint中的print()方法。
@Override
public void print() {
System.out.println("匿名内部类");
}
});
}

public static void fun(IPrint msg) {
msg.print();
}

}

这样就相当于创建了一个一次性的子类,简化了实例化抽象类接口的操作。可能由于这个类是在一个类内部声明的,所以被叫作内部类。但实际上它跟真正的内部类关系不大,相反具有更多子类的性质,所以叫匿名子类或者匿名类更合适。其匿名内部类的名称可能只是一个惯例,其来历这里就不深究了,读者只需理解它的性质,不要被它的名称搞晕即可。

函数式接口

我们把只定义了一个抽象方法的接口称之为函数式接口(Functional Interface),用注解@FunctionalInterface标记。例如,Callable接口:

1
2
3
4
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

再来看Comparator接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface
public interface Comparator<T> {

int compare(T o1, T o2);

boolean equals(Object obj);

default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}

default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是普通方法(default方法)或静态方法(static方法)。另外注意到boolean equals(Object obj)Object定义的方法,不算在接口方法内。因此,Comparator接口也是一个函数式接口。

由于只有一个抽象方法,函数式接口可以用Lambda表达式来实例化。

Lambda 表达式

应用在单一抽象方法(Single Abstract Method, SAM)接口环境下的一种简化定义形式,用于解决匿名内部类的定义复杂问题。

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
@FunctionalInterface
interface IPrint {
// 单一抽象方法接口
void print();
}
@FunctionalInterface
interface ICalc {
//单一抽象方法接口
int calc(int x, int y);
}

public class Lambda {

public static void main(String[] args) {
// 匿名内部类写法
fun(new IPrint () {
@Override
public void print() {
System.out.println("匿名内部类");
}
});

// Lambda表达式
fun1(() -> System.out.println("Lambda"));
//方法体为多行代码时打大括号
String info = "信息";
fun1(() -> {
System.out.println("题目");
System.out.println(info);
});

//覆写有参数有返回值的方法
fun2((s1, s2) -> {
return s1 + s2;
});
//可以省略return,方法体就是一个表达式
fun2((s1, s2) -> s1 + s2);
}

public static void fun1(IPrint msg) {
msg.print();
}
public static void fun2(ICalc msg) {
System.out.println(msg.calc(2, 10));
}
}

假如fun1(IPrint msg)fun2(ICalc msg)方法不能或不应该修改,就能体现出函数式编程的灵活性了。可以通过编写 Lambda 表达式来随意设置fun2(ICalc msg)方法中对2和10两个数字的计算方式。想改成乘法,乘方,甚至从2加到10等等……

方法引用

在Java8中,我们可以直接通过方法引用(Method Reference)这一特性来简写 Lambda 表达式。方法引用用来直接访问类或者实例的已经存在的方法或者构造方法。 方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。

计算时,方法引用会创建函数式接口的一个实例。 当 Lambda 表达式中只是执行一个方法调用时,不用 Lambda 表达式而是直接用方法引用可读性更高。 方法引用是一种更简洁易懂的 Lambda 表达式

两个方法的方法参数的数量和类型一致,返回类型相同,我们就说这两者方法签名一致。不看方法名称,也不看类的继承关系。如果某个方法的签名和接口恰好一致,就可以直接传入方法引用。

方法引用的操作符是双冒号::

  • Lambda表达式的写法:

    1
    2
    3
    Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
    }
  • 引用静态方法:类名称::静态方法名称

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Main {

    public static void main(String[] args) {
    String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
    Arrays.sort(array, Main::cmp);// 静态方法
    System.out.println(String.join(", ", array));
    }

    static int cmp(String s1, String s2) {
    return s1.compareTo(s2);
    }

    }
  • 引用特定类型的方法:特定类::普通方法

    注意,如果引用了String.compareTo()这种实例方法,需要一个本类的对象A来调用,比如:A.compareTo(B)。观察String.compareTo()方法:

    1
    2
    3
    4
    5
    public final class String {
    public int compareTo(String o) {
    ...
    }
    }

    这个方法的签名只有一个参数,但实际上可以作为参数传入。能与int Comparator<String>.compare(String, String)匹配。

    1
    Arrays.sort(array, String::compareTo);

    为什么?因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用时,第一个隐含参数总是传入this,相当于静态方法:

    1
    public static int compareTo(this, String o);
  • 引用某个对象的方法:实例化对象::普通方法

    也是引用实例方法,故同上。

  • 引用构造方法:类名称::new

    构造方法虽然没有return语句,但它会隐式地返回this实例

总结

无论匿名内部类、Lambda 表达式还是方法引用,都是将一个签名相对应的函数作为参数传入方法中,来灵活地参与方法内的运算,进而简化代码编写。