在java中,内置了@FunctionalInterface注解,这个注解的作用网络上有一大堆的文章解释,我就不赘述了。同样的基于这个注解,java也内置了一批函数式接口,网络上也有一大堆的文章解释。我打算从另外一个角度来阐述下这些接口。
"函数"
在函数式编程中,一个函数必须是有一个入参,有一个出参。 在数学中,对应定义域和陪域,在上一篇中,我们也可以提到,抛开函数内部进行的运算,这两个一一对应的值,也可以视作有序对。这里有个观念是需要破除的,就是入参和出参,在高中时期,我们还是一般认为函数只能处理数字的。但是实际入参和出参并不限制在数字上,限制数字的是某个函数。也就是说,函数限制定义域和陪域。而定义域和陪域限制的体现之一,就是类型。后面我们讲。 之后所有的Java内置函数式接口,我们都将基于这个思路进行思考。
Function<T,R>
这个函数的作用是: 接收一个值,类型为T(泛型); 返回一个值,类型为R(泛型); 这个接口完美符合函数的定义,首先,我们来观察这么一个函数: Function<Integer,Integer> 很明显,它接收一个Integer和返回一个Integer。那么在Java中Integer有什么特点呢? 它的取值范围在2147483647到‑2147483648。所以这个Function,它的定义域和陪域均落在了这个范围之内,这个函数只能处理2147483647到‑2147483648之间的数字,也只能返回这个范围之内的数字。 接下来我们再观察这么一个函数: Function<Integer,String> 这里的定义域和陪域的取值范围就不一样了。定义域依然落在了147483647到‑2147483648之间。但是返回的是String。那么String在Java中的定义是什么?String的上限是内存,这实际意味着定义域的类型限制是个有限取值,而陪域的类型限制是个无限取值。你的入参只能映射一部分出参的数据,无法做到一一映射,目前看来,不是什么大问题。先不管。那么函数内部的具体运算呢?我举个例子你就明白了。1->"1",这是一种运算。1->"一",这是第二种运算。1->"壹",这是第三种运算。实际上,1->"1"和1->"1"都可能是不同的运算,但是这两个结果如果是不同运算,这意味着有些不好的东西出现了,这个我们后面再思考。 接下来我们再观察这么一个函数: Function<String,Integer> 我们调换了出入参的类型,现在入参是无限取值,而返回的是有限取值了。情况有点棘手起来了,但也并非没法完全处理。比如"1"->1,"hello"->0,"你好"->0,"2"->2。这个运算还是一目了然的。但是同样的,还有一种方式,我们再定义一种类型,叫IntString。当我们new IntString("hello")的时候,直接抛出异常。我们只接收可以被Integer.valueOf的文本,其它类型的文本一概不接受。这样我们就通过一个类型,限制住了调用者可以向函数写入参数的限制。所以当我们看到如下函数的时候: Function<User,Authority> 或者 Function<User,Team> 也就是说,这个函数接口,在限制出入参可取值的范围。只要在这个范围之内,这个函数就能处理。如果不能,那说明有bug。(不过,也有例外,这个不在本篇范围内讲)
Predicate
这个函数的作用是: 接收一个值,类型为T(泛型); 返回一个值,类型为Boolean; ?那不就是 Function<T,Boolean> 是的,这两个就是一回事。Predicate是特化的Function。或者说是类型化后的Function,当然也有自己类型化的用法。
BiFunction<T, U, R>
这个函数的作用是: 接收两个值,一个类型为T(泛型),一个类型为U; 返回一个值,类型为R(泛型); 嗯,这似乎没有什么办法了?请出Currying。 我们前面说过不能认为函数只能处理数字,我们前面的例子,函数也处理了文本。那么,函数也能处理函数,当我们用这个思路来转换这个BiFunction的时候,就触发了Currying。观察下面这个函数: Function<T,Function<U,R>> 入参是T类型,你可以得到一个函数Function<U,R>,接着对这个入参U类型的函数进行调用,你得到了R。也就是说,我们可以把一个BiFunction(同时两个输入)转换为Function的嵌套(连续两个输入,通过中间产生一个Function连接)。Currying是个非常有用的技巧,可以参考我这篇文章一个报表数据写入的函数式接口(Java)。尤其是你在处理两个入参存在树状关键数据的时候。
Consumer
这个函数的作用是接收一个值,类型为T(泛型)。但是没有东西返回。 没有东西返回,在Java中也可以表示为void,也就是说,他返回的是类型是Void。有些类型的比较有意思的,Void下面只有一个值void,而void表示没有值。有的比如Boolean,下面只有两个值。有的比如IntString,下面只有。。。 回过头来看。Consumer和Function<T,Void>,你认为是不是同一个东西? 是的,这两个就是一回事。Consumer是特化的Function。 对吧,所以的定义域都会映射到陪域上,而陪域类型所限制的值只有一个值void。 但是如果我们不把void当成是一个值,那么Consumer就变成了一个奇怪的东,你不往里面放任何东西,但是它却能喷涌而出五花八门的东西。从现象上看,这就是副作用。 目前看来似乎不是什么问题,我们继续下一个。
Supplier
这个函数的作用是不接受任何值,但是返回T。套用我们在Consumer上的理解,可以把它理解成以下函数: Function<Void,T> 那么假设我们定义了这么一个函数 Function<Void,Boolean> 这产生了一个大问题,定义域只有一个值,而陪域不是。如果我们像之前Function<String,Integer>一样的方式通过定义一个新类型的方式限制Boolean,只能是flase或true的类型。那么这个函数的意义何在。我们只有一个有序对,即Pair.of(void,false)或Pair.of(void,true)。我们永远知道结果,而不需要调用函数进行执行(直接记忆化了,上篇讲过,可以回看)。如果,Function<Void,Boolean>表示了我们执行了一个网络ping操作,如果成功,返回true,失败返回flase。那么定义域的值无法唯一映射到陪域上的某个值,记忆化失败了,函数也失败。这意味着副作用,Supplier背后不管是Ping的网络操作,还是数据库操作,这东西似乎天生就和副作用绑死在一起了。Supplier让我们前面的思路似乎有点问题。(不过,副作用也不是不能处理,这个不在本篇范围内讲)
总结
本篇我们通过另外一个视角,梳理了常见的几个Java内置函数式接口。基本上把所有的接口,引导向了最初的那个一个入参,一个出参的函数定义的接口Function<T,R>。也从限定陪域和定义域的方向,从另外一个角度来看待了一遍类型。需要注意,这只是一种角度,而不是全部或本质。但是也通过Supplier和Consumer类型的接口,引出了副作用的问题,而这个问题将在下一篇进行解释,就是那个Monad。也就是"A monad is a monoid in the category of endofunctors, what's the problem?"。 待续。。。