欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java函数式编程学习笔记(一)

程序员文章站 2022-03-03 11:58:24
...

1. 此函数非彼函数

在弄清楚什么是函数编程之前,有必要先弄清楚究竟什么是函数这个问题。在面向对象编程中,我们经常将方法称为函数,那么方法与函数究竟是否是同一个东西的不同称呼呢?函数式编程中的“函数”是指数学意义上的函数,不是编程语言中的“函数”。数学上的函数(Function)可以看成一个小机器,给这个机器提供一定的原材料(输入参数),它就会加工出(输出)一定的产品(返回值),如图1所示。

图1 函数示意图

Java函数式编程学习笔记(一)
            
    
    博客分类: 函数式编程并发编程 函数式编程并发编程图片来自: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