概述
函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。
Lambda 表达式 ,简单来讲就是,Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。在Java中,不能返回函数。
用法
在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
- Comparator
- Runnable
- Callable
以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例,以匿名类方式编写如下:
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
采用lambda表达式比这种匿名内部类的写法大大简化了。
观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
由此可以看出,lambda表达式的关键在于输入和输出,即参数和返回值,方法名可以省略。
如果括号中的语句只有一行,还可以简写:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
输入参数的类型和返回值的类型都是由编译器自动推断的,这里推断出的返回值是int
,因此,只要返回int
,编译器就不会报错。
相比于匿名内部类的情况,lambda表达式,省略了class的定义,省略了输入输出参数类型的,省略了方法名,只需要专注于方法体的编写,简化了代码。
原理
既然lambda表达式如此好用,那么如何定义一个方法,使其可以使用lambda表达式作为输入参数呢?
Lambda表达式,本质上是一个匿名方法,在Java1.8中使用单方法接口来表示,并定义了一个新的注解:@FunctionalInterface来进行标识。
我们来看看Arrays.sort()是如何定义的:
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
这里可以看到,sort方法接收一个数组和一个 Comparator,我们再来看Comparator:
@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) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
...
}
这是一个单方法接口,标注了@FunctionalInterface,只有一个抽象方法compare。(equals属于Object,不算)
自定义
这里我们使用一个例子来定义应用lambda表达式:
这里我们设计一个int数组自定义变换后求和的接口,这里默认是数组所有元素加总求和,但是可以自定义数组中的元素经过变换(如加倍,乘方)后求和。
定义一个函数式接口:
@FunctionalInterface
public interface Compute {
int compute(int number);
}
调用接口的类:
public class MyList {
public static int mySum(int[] numbers, Compute compute){
if (compute == null){
return sum(numbers);
}else {
return Arrays.stream(numbers).map(compute::compute).sum();
}
}
public static int sum(int[] numbers){
return Arrays.stream(numbers).sum();
}
}
测试:
public class Main {
public static void main(String[] args) {
int[] numbers = {1,2,3};
int result = MyList.mySum(numbers, null);
int doubleResult = MyList.mySum(numbers, number -> number * 2);
System.out.println("result : " + result);
System.out.println("doubleResult: " + doubleResult);
}
}
这里可以看到,默认的输出是加总和,可以自定义为 number * 2 后加和,注意这里的lambda表达式为:number -> number * 2,这里省略了方法名,输入输出参数类型。
与接口方法 int compute(int number) 保持一致就可以了。
这里的lambda表达式还可以独立成方法,如下:
public class Main {
public static void main(String[] args) {
int[] numbers = {1,2,3};
int result = MyList.mySum(numbers, null);
int doubleResult = MyList.mySum(numbers, Main::multiply);
System.out.println("result : " + result);
System.out.println("doubleResult: " + doubleResult);
}
public static int doubleValue(int number){
return number * 2;
}
public static int multiply(int number){
return number * number;
}
}
这里用Main::multiply,即类名::方法名来简写。
因为Compute接口定义的方法是int compute(int)
,和静态方法int multiply(int)
相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入:
MyList.mySum(numbers, Main::multiply);
注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
构造对象
lambda表达式还有一个很神奇的应用,就是引用构造方法创建对象数组,即批量创建对象。
这个需要在了解了Stream之后使用,具体参考: