Java函数式编程学习笔记(一)
1. 此函数非彼函数
在弄清楚什么是函数编程之前,有必要先弄清楚究竟什么是函数这个问题。在面向对象编程中,我们经常将方法称为函数,那么方法与函数究竟是否是同一个东西的不同称呼呢?函数式编程中的“函数”是指数学意义上的函数,不是编程语言中的“函数”。数学上的函数(Function)可以看成一个小机器,给这个机器提供一定的原材料(输入参数),它就会加工出(输出)一定的产品(返回值),如图1所示。
图1 函数示意图
图片来自:https://en.wikipedia.org/wiki/Function_(mathematics)
方法往往是有副作用(Side Effect)的,即一个方法的调用往往意味着对象状态的变化,而这种变化可能使得用同样的参数多次调用同一个方法,每次调用的返回值可能各不相同。而数学上的函数是没有副作用的(Side Effect Free),因此只要使用同样的参数调用同一个函数,那么不管调用这个函数多少次,每次调用的返回值总是相同的。例如,对于一个计算矩形面积的函数f(x,y)=x*y(x和y代表矩形的长和宽),那么无论调用多少次f(3,4),每次的返回值总是12。
从“无副作用”的角度来看,一个方法只要对其的调用不会产生副作用,那么它就是一个函数,否则它就不是函数。例如,String.charAt(int)这个方法就是一个函数。System.currentTimeMillis()这个方法就不是函数,因为对其的每次调用的返回值都可能不相同。
2. 函数式编程:函数作为“一等公民”
面向对象编程是Java平台在Java 8之前就已经支持的一种编程范式(Paradigm),Java平台从Java 8开始支持另外一种编程范式——函数式编程(Functional Programming)。面向对象编程中,对象是“一等公民”。所谓“一等公民”就是指可以作为“值”的语言元素。“值”既可以赋值给变量,也可以作为方法调用的参数进行传递以及作为方法的返回值。例如,我们可以将“1024”赋值给一个int型变量,也可以将其作为方法调用的参数,或者作为一个方法的返回值。同样,对象(确切的说是对对象的引用,相当于指针)也可以作为一个变量(引用型变量)值或者作为一个方法调用的参数,或者作为一个方法的返回值(返回一个对象的方法我们通常称之为工厂方法)。
在面向对象编程中,方法(或者函数)并非“一等公民”,因为方法在这里就不是一个值。而在函数式编程中,函数翻身成为“一等公民”,因此在这里函数可以作为一个值赋值给一个变量,可以作为函数调用的参数(即用函数作为参数去调用一个函数),可以作为函数的返回值(即一个函数的返回值是另外一个函数)。
3. 函数式编程的优势
3.1. 编写更为简洁的代码
函数式编程使得我们能够编写更为简洁的代码。下面看一个例子。假设有个服务(如清单1所示),其启动动作(即Service.start()方法)比较耗时,因此我们希望用异步的方式去启动这个服务,以避免主线程被阻塞。
清单1 一个启动比较耗时的服务
public class SomeTimeConsumingService implements Service {
@Override
public void start() {
// 模拟启动动作的耗时
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
;
}
}
}
为此,我们只需要创建一个专门的线程(工作者线程),并在该线程的run方法中调用Service.start()即可,如清单2所示。
清单2 以异步方式启动一个耗时服务(面向对象编程)
public class ServiceStarter {
public static void main(String[] args) {
Thread serviceStarter;
final SomeTimeConsumingService service = new SomeTimeConsumingService();
//创建Runnable实例以调用Service.start()
serviceStarter = new Thread(new Runnable() {
@Override
public void run() {
service.start();
}
});
//启动线程
serviceStarter.start();
}
}
清单2中我们创建的匿名Runnable实例的目的仅仅是为了在工作者线程中调用Service.start()而已。如果我们能够直接把Service.start()方法作为一个参数传递给Thread类的构造器(姑且将构造器看做一种特殊的方法),那么这里我们也就省却了创建Runnable实例,从而使代码更加简洁。在Java 8之后我们的确可以这么做,如清单3所示。
清单3 以异步方式启动一个耗时服务(函数式编程)
public class ServiceStarterFP {
public static void main(String[] args) {
Thread serviceStarter;
final SomeTimeConsumingService service = new SomeTimeConsumingService();
serviceStarter = new Thread(service::start);//直接将start方法作为参数传递给构造器
serviceStarter.start();
}
}
这里,“service::start”是一个方法引用(Method Reference),它表示对象service的start方法。可见,我们将一个方法作为一个参数传递给了Thread类的构造器,这使得清单3相比清单2中相应代码要简洁一些。读者也许在想,Thread类的构造器的参数的类型是java.lang.Runnable,何以我们能够将一个方法(作为“值”)作为其参数传入呢?这样为何不会出现类型不匹配(兼容)的问题呢?后续的笔记中我们会解释这一点。
从这个例子我们可以看到,Java 8中方法已然跻身作为函数的“一等公民”行列,因此方法可以作为参数进行传递。显然Service.start()并不是真正意义上函数,因为它是有副作用的(至少它有可能是有副作用的,因为它是用来启动一个服务的)。尽管如此,这个方法还是享受到了函数的“一等公民”待遇(作为值传递)。由此可见,Java 8中一个方法可以看做是一个函数,但是Java并不强制这个方法必须是无副作用的。
3.2. 函数式编程为并发编程提供了便利
函数式编程为并发编程提供了便利。函数是无副作用的,这就意味着这函数必须是无状态(Stateless)。无状态是多线程编程和函数式编程的共同好友。我们知道,在多线程编程中,如果多个线程之间存在共享可变状态(Shared Mutable State),那么为了确保线程安全我们往往需要借助锁,而锁除了其开销较大之外还存在可能导致死锁等问题。相反,如果多个线程之间不存在共享状态(或者仅存在只读状态),那么这些线程是可以并行(Parallel)的。这不仅有利于提高系统的并发性,也使得代码更为简单。函数也是类似,既然函数是(必须是)无状态的,那么多个线程同时执行一个函数的时候也就无需加锁,这既简化了代码又有利于提供系统的并发性。
4. 作者简介
黄文海,著有《Java多线程编程实战指南(核心篇)》、《Java多线程编程实战指南(设计模式篇)》。
5. 参考资料
1.Raoul-Gabriel Urma等.Java 8实战.人民邮电出版社,2016
2.黄文海.Java多线程编程实战指南(核心篇).电子工业出版社,2017
3.黄文海.Java多线程编程实战指南(设计模式篇).电子工业出版社,2015