Java 函数式编程基础
函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称 Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为 Lambda 计算。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。
虽然Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数。从 Java 8 开始,Java 平台支持函数式编程。
将一个实例传入一个方法中,实例的成员方法就相当于作为参数传入其他函数的函数,这样就可以实现函数式编程。
// 定义接口
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)本身的前提下改变传入到方法的“函数”,进而改变方法的执行结果,实现传入普通参数难以达到的灵活性。比如:
class Print implements IPrint {
public void print() {
// 覆写IPrint中的print()方法。
System.out.println("打印普通信息");
System.err.println("打印错误信息");
}
}
同时打印普通信息和错误信息,甚至做出更复杂的效果,这是只接收字符串等参数传入的方法无法灵活做到的。以往想要改变一个方法的逻辑,必须要改写方法本身,现在只需要改变传入的“函数”即可。
匿名内部类
如果Print类只使用一次,还要将其定义为一个具体的类,就很麻烦。这时就可以用到匿名内部类了。
内部类是指在类内部定义的类结构,利用内部类可以方便地实现私有属性的互相访问,一般的内部类需要明确地使用class进行定义,而匿名内部类较为特殊,是没有名字的类。必须在抽象类或接口的基础上才能定义,可以实例化抽象类或接口,适合用来创建一次性子类。
语法:new 抽象类名或接口名() {},大括号内部的内容即是该实例化子类的内容。
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接口:
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
再来看Comparator接口:
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator reversed() {
return Collections.reverseOrder(this);
}
default Comparator 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 表达式能简化匿名内部类的书写,但并不能取代所有匿名内部类,只能取代函数式接口的简写。
姑且将其看作一种匿名函数,没有声明的方法,即没有访问修饰符、返回值声明、和名字。它允许你将函数作为参数传递进方法中。使代码更加简洁紧凑。
语法:(参数) -> 方法体
@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表达式的写法:
Arrays.sort(array, (s1, s2) -> { return s1.compareTo(s2); } -
引用静态方法:
类名称::静态方法名称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()方法:public final class String { public int compareTo(String o) { ... } }这个方法的签名只有一个参数,但实际上可以作为参数传入。能与
int Comparator匹配。.compare(String, String) Arrays.sort(array, String::compareTo);为什么?因为实例方法有一个隐含的
this参数,String类的compareTo()方法在实际调用时,第一个隐含参数总是传入this,相当于静态方法:public static int compareTo(this, String o); -
引用某个对象的方法:
实例化对象::普通方法也是引用实例方法,故同上。
-
引用构造方法:
类名称::new构造方法虽然没有
return语句,但它会隐式地返回this实例
总结
无论匿名内部类、Lambda 表达式还是方法引用,都是将一个签名相对应的函数作为参数传入方法中,来灵活地参与方法内的运算,进而简化代码编写。