函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称 Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为 Lambda 计算。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。
虽然Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this
参数的函数。从 Java 8 开始,Java 平台支持函数式编程。
将一个实例传入一个方法中,实例的成员方法就相当于作为参数传入其他函数的函数,这样就可以实现函数式编程。
1 | // 定义接口 |
只需要编写IPrint
接口的不同实现子类,设置其中的成员方法内容,就可以在不动方法fun(IPrint msg)
本身的前提下改变传入到方法的“函数”,进而改变方法的执行结果,实现传入普通参数难以达到的灵活性。比如:
1 | class Print implements IPrint { |
同时打印普通信息和错误信息,甚至做出更复杂的效果,这是只接收字符串等参数传入的方法无法灵活做到的。以往想要改变一个方法的逻辑,必须要改写方法本身,现在只需要改变传入的“函数”即可。
匿名内部类
如果Print
类只使用一次,还要将其定义为一个具体的类,就很麻烦。这时就可以用到匿名内部类了。
内部类是指在类内部定义的类结构,利用内部类可以方便地实现私有属性的互相访问,一般的内部类需要明确地使用class进行定义,而匿名内部类较为特殊,是没有名字的类。必须在抽象类或接口的基础上才能定义,可以实例化抽象类或接口,适合用来创建一次性子类。
语法:new 抽象类名或接口名() {}
,大括号内部的内容即是该实例化子类的内容。
1 | interface IPrint { |
这样就相当于创建了一个一次性的子类,简化了实例化抽象类或接口的操作。可能由于这个类是在一个类内部声明的,所以被叫作内部类。但实际上它跟真正的内部类关系不大,相反具有更多子类的性质,所以叫匿名子类或者匿名类更合适。其匿名内部类的名称可能只是一个惯例,其来历这里就不深究了,读者只需理解它的性质,不要被它的名称搞晕即可。
函数式接口
我们把只定义了一个抽象方法的接口称之为函数式接口(Functional Interface),用注解@FunctionalInterface
标记。例如,Callable
接口:
1 |
|
再来看Comparator
接口:
1 |
|
虽然Comparator
接口有很多方法,但只有一个抽象方法int compare(T o1, T o2)
,其他的方法都是普通方法(default方法)或静态方法(static方法)。另外注意到boolean equals(Object obj)
是Object
定义的方法,不算在接口方法内。因此,Comparator
接口也是一个函数式接口。
由于只有一个抽象方法,函数式接口可以用Lambda表达式来实例化。
Lambda 表达式
应用在单一抽象方法(Single Abstract Method, SAM)接口环境下的一种简化定义形式,用于解决匿名内部类的定义复杂问题。
Lambda 表达式能简化匿名内部类的书写,但并不能取代所有匿名内部类,只能取代函数式接口的简写。
姑且将其看作一种匿名函数,没有声明的方法,即没有访问修饰符、返回值声明、和名字。它允许你将函数作为参数传递进方法中。使代码更加简洁紧凑。
语法:(参数) -> 方法体
1 |
|
假如fun1(IPrint msg)
,fun2(ICalc msg)
方法不能或不应该修改,就能体现出函数式编程的灵活性了。可以通过编写 Lambda 表达式来随意设置fun2(ICalc msg)
方法中对2和10两个数字的计算方式。想改成乘法,乘方,甚至从2加到10等等……
方法引用
在Java8中,我们可以直接通过方法引用(Method Reference)这一特性来简写 Lambda 表达式。方法引用用来直接访问类或者实例的已经存在的方法或者构造方法。 方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。
计算时,方法引用会创建函数式接口的一个实例。 当 Lambda 表达式中只是执行一个方法调用时,不用 Lambda 表达式而是直接用方法引用可读性更高。 方法引用是一种更简洁易懂的 Lambda 表达式。
两个方法的方法参数的数量和类型一致,返回类型相同,我们就说这两者方法签名一致。不看方法名称,也不看类的继承关系。如果某个方法的签名和接口恰好一致,就可以直接传入方法引用。
方法引用的操作符是双冒号::
。
Lambda表达式的写法:
1
2
3Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
}引用静态方法:
类名称::静态方法名称
1
2
3
4
5
6
7
8
9
10
11
12
13public 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
5public 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 表达式还是方法引用,都是将一个签名相对应的函数作为参数传入方法中,来灵活地参与方法内的运算,进而简化代码编写。