2021-03-25
< view all posts关于FuntionalInterface的介绍很多是单纯从语言特性的角度讲解,其实我觉得结合具体场景和设计思路来理解会更清晰一些。比如说在开发中有一些场景,如果方法能够作为参数传递的话那就会十分的便利,在这种时候函数式接口的使用就显得很自然了。
什么时候会有这个传递方法的需求呢,以一个业务场景来举例子:我们目前设计的系统后台需要对另一个系统进行操作,因为网络或者系统的波动,操作可能会失败,比如发生HTTP ERROR,或者对方系统返回了报错等等,出现这种情况时希望能够自动重试几次,当重试全部失败后才认为操作失败。可能有很多个不同的操作都需要这个重试的功能,同时,程序中的其他一些地方也可能能够抽象出“重试”的逻辑,比如确实需要进行多次的业务,或者是对某个地址做轮询之类。
为了避免重试这个逻辑在不同的方法中重复实现,自然的想法是把它独立出来。一个既简单而又有很强泛用性的抽象逻辑是: retry(method, times)。这里就需要把方法作为参数传递,也就是需要函数式的特性。这里插一句,用纯OOP的方式能不能实现呢,当然也是可以的。但是问题是,1.实现起来不简洁,2.泛用性不强。而Spring的类似的重试功能的实现,以及Java内部的Thread类等等地方,都是用了函数式接口在这种与某类方法关系更密切、而不是很关心具体对象的场景,可以看出这种函数式的方法确实有其优势。
那么问题来了,怎么在Java中传递一个方法?不妨先来看一看如果是一个纯函数式语言,比如Scala的话,这个逻辑要怎么写。
为了例子更加简洁,我们把实际的业务逻辑替换成一个0-1的随机浮点数生成方法,现在希望把它传递给另一个方法,判断生成的随机数大小。如果小于0.9的话就重试,直到得到一个大于0.9的随机数。用Scala可以很简单地写出来:
def generateRandomDouble(): Double = { val r = scala.util.Random r.nextDouble() } def retry(method: () => Double): Double = { val number: Double = method() if(number < 0.9) retry(method) else number } retry(generateRandomDouble)
这里 generateRandomDouble() 就是需要进行重试的方法,而重试的逻辑则在 retry() 方法中。因此将方法传递给 retry() 就可以完成重试。这里有两个值得注意的地方,一是传递进来的方法,它的类型的表示方式为 () => Double ,因为在Scala中函数是一等公民,所以函数和变量一样,都有对应的类型表示方式,左边的空括号表示这个函数不接受参数,右边的Double表示它的返回类型。第二个值得注意的点是在函数体中对传递进来的函数的调用方式,非常简单,就是 method() ,这是因为函数可以单独定义,所以可以直接这样调用。
那么同样的逻辑,在Java中要怎样写呢。这里我们就遇到了两个和上面相对应的问题,一是在Java中,方法不是基本类型,作为参数传递的话没有办法表示方法的类型;二是Java中方法必须依附于类或者对象,无法直接通过“方法名()”去调用。
正是为了解决这样的问题,在Java8中引入了函数式接口的概念。在看函数式接口之前,我们先把程序的其它部分用Java写好:
import java.util.Random; class Generator { double generateRandomDouble() { Random rand = new Random(); return rand.nextDouble(); } } class Retry { double attempt(method){ double number = method(); while (number < 0.9){ number = method(); } return number; } } public class Example{ public static void main(String[] args) { Generator g = new Generator(); Retry retry = new Retry(); System.out.println(new Retry.attempt(???)); } }
类似的,我们定义了一个生成随机数的类,和一个进行重试的类Retry,但是有两个地方会报错,一是Retry类中attempt()方法的定义,另一个是main()中对attemp()方法的调用。
首先解决调用时如何将方法作为参数传递的问题,我们知道Java8提供了lambda表达式,因此可以把对方法的调用写成 () -> g.generateRandomDouble() 的形式。它表示这个lambda表达式不接受参数,直接返回随机数生成的结果。特别的,对这种不需要接受参数的lambda,Java提供了一个语法糖,我们可以把它写成 g::generateRandomDouble。
可以传递lambda表达式了,下面需要解决的问题就是接受的部分要如何去处理。这里就要用到FunctionalInterface:既然方法的传递必须要依附于对象,那就创建一个临时的对象,让它作为方法的载体。FunctionalInterface就是给这个对象实现的接口。而调用FunctionalInterface中定义的抽象方法,就等于调用了传递进来的方法。因此完整的程序可以写成这样:
import java.util.Random; @FunctionalInterface interface Retryable{ abstract double dummyMethod(); } class Generator { double generateRandomDouble() { Random rand = new Random(); return rand.nextDouble(); } } class Retry { double attempt(Retryable dummyObj){ double number = dummyObj.dummyMethod(); while (number < 0.9){ number = dummyObj.dummyMethod(); } return number; } } public class Example{ public static void main(String[] args) { Generator g = new Generator(); Retry retry = new Retry(); System.out.println(new Retry().attempt(g::generateRandomDouble)); } }
因为实现FunctionalInterface的实际上是一个为了传递方法而临时创建的对象,所以我将这个对象和方法起名为 dummyObj.dummyMethod()。
最后思考一点,因为需要创建一个临时对象去作方法的调用,Java是如何保证传递进去的方法和临时对象的方法的对应关系的?在我们的代码里面,并没有对这两者的方法名作任何的关联。如果FunctionalInterface里面定义了两个以上的抽象方法,比如dummyMethod1和dummyMethod2,Java是如何知道调用哪个方法才表示要调用g.generateRandomDouble()呢?
实际上Java并没有用什么巧妙的方法,而是直接规定了FunctionalInterface里面必须有且仅有一个抽象方法。所以方法的对应并不是靠方法名字,而靠规定抽象方法的唯一性来保证的。
当然,这个方法最好还是有一个符合语义的名字。尽管我们这里完全没有使用 implements 关键字去实现这个函数式接口(而是在接受参数时隐式地实现了它)但是直接用 implements 去实现它当然也是可以的。以Java自带的Thread类为例,Runnable就是一个函数式接口,我们可以直接用 Thread(someClass::someMethod) 的方式传递一个方法给它,也可以选择构造一个类,实现Runnable接口并且重写里面抽象的Run()方法,之后传递一个实例化的对象给它。
可见虽然有一些曲折和别扭,不过Java通过函数式接口还是实现了函数式和OOP的统一,为我们编程提供了方便。