函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。
Lambda表达式
在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
Comparator
Runnable
Callable
以Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例,以匿名类方式编写如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:
// Lambda
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
}
观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
其中,参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { ... }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁。
如果只有一行return xxx的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错。
FunctionalInterface
我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
再来看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) {
...
}
...
}
虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。
小结
单方法接口被称为FunctionalInterface。
接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。
Lambda表达式的参数和返回值均可由编译器自动推断。
方法引用
实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:
import java.util.Arrays;
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);
}
}
上述代码在Arrays.sort()中直接传入了静态方法cmp的引用,用Main::cmp表示。
因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。
因为Comparator
Arrays.sort(array, Main::cmp);
注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
我们再看看如何引用实例方法。如果我们把代码改写如下:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, String::compareTo);
System.out.println(String.join(", ", array));
}
}
不但可以编译通过,而且运行结果也是一样的,这说明String.compareTo()方法也符合Lambda定义。
观察String.compareTo()的方法定义:
public final class String {
public int compareTo(String o) {
...
}
}
这个方法的签名只有一个参数,为什么和int Comparator
因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:
public static int compareTo(this, String o);
所以,String.compareTo()方法也可作为方法引用传入。
构造方法引用
除了可以引用静态方法和实例方法,我们还可以引用构造方法。
我们来看一个例子:如果要把一个List
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = ???
传统的做法是先定义一个ArrayList
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
persons.add(new Person(name));
}
要更简单地实现String到Person的转换,我们可以引用Person的构造方法:
// 引用构造方法
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}
后面我们会讲到Stream的map()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
把泛型对应上就是方法签名Person apply(String),即传入参数String,返回类型Person。而Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是类名::new,因此,此处传入Person::new。
小结
FunctionalInterface允许传入:
接口的实现类(传统写法,代码较繁琐);
Lambda表达式(只需列出参数名,由编译器推断类型);
符合方法签名的静态方法;
符合方法签名的实例方法(实例类型被看做第一个参数类型);
符合方法签名的构造方法(实例类型被看做返回类型)。
FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。
实例
在下面main中新增的4个订阅者采用了不同的写法,效果是等价的。
public class NewsDemo {
public static void main(String[] args) {
//模拟有一家新闻社
NewsAgency bbc = new NewsAgency("BBC");
//订阅该社的新闻-普通
bbc.addListener(new MyListener());
//订阅该社的新闻-匿名类
bbc.addListener(new Listener() {
@Override
public void newsArrived(NewsEvent e) {
if (e.level > 5) System.out.println("warning :");
System.out.println("please note," + e.text + " happened!");
}
});
//订阅该社的新闻-lambda
bbc.addListener(e -> {
if (e.level > 5) System.out.println("warning :");
System.out.println("please note," + e.text + " happened!");
});
//订阅该社的新闻-方法引用
bbc.addListener(new MyListener()::newsArrived);
//新闻社启动其工作流程
bbc.start();
}
}
/**
* 事件信息
*/
class NewsEvent {
Object source; //事件来源
String text; //事件内容
int level; //事件级别
NewsEvent(Object source, String text, int level) {
this.source = source;
this.text = text;
this.level = level;
}
}
interface Listener {
void newsArrived(NewsEvent e);
}
/**
* 新闻机构
*/
class NewsAgency {
String name; //机构名
public NewsAgency(String name) {
this.name = name;
}
Listener[] listeners = new Listener[100]; //侦听者(订阅者)
int listenerCnt = 0; //已有的订阅者
//加入一个订阅者
void addListener(Listener oneListener) {
if (listenerCnt < listeners.length) {
listeners[listenerCnt] = oneListener;
listenerCnt++;
}
}
//模拟一个事件发生,并通知所有的订阅者
void start() {
NewsEvent event = new NewsEvent("Mr. Joan", "Bombing", 9);
for (int i = 0; i < listenerCnt; i++) {
listeners[i].newsArrived(event);
}
}
}
/**
* 实现一个订阅者
*/
class MyListener implements Listener {
//当接收到消息后,进行一些显示
public void newsArrived(NewsEvent e) {
if (e.level > 5) System.out.println("warning :");
System.out.println("please note," + e.text + " happened!");
}
}