前言
函数式编程是一种历久弥新的编程范式,比起命令式编程,它更加关注程序的执行结果而不是执行过程。Guava 做了一些很棒的工作,搭建了在 Java 中模拟函数式编程的基础设施,让我们不用多费手脚就能享受部分函数式编程带来的便利。
Java 始终是一个面向对象(命令式)的语言,在我们使用函数式编程这种黑魔法之前,需要确认:同样的功能,使用函数式编程来实现,能否在健壮性和可维护性上,超过使用面向对象(命令式)编程的实现?
Function
Function 接口是我们第一个介绍的 Guava 函数式编程基础设施。
下面这段代码是去掉注释之后的 Function 接口。
@GwtCompatible
public interface Function<F, T> {
@Nullable T apply(@Nullable F input);
@Override
boolean equals(@Nullable Object object);
}
实例化这个仿函数接口要求至少要实现 apply 方法。只有在需要判断两个函数是否等价的时候才覆盖实现 equals 方法。
下面我们通过一个简单的函数定义的例子看看 Function 接口的用法。
Function<Double, Double> sqrt = new Function<Double, Double>() {
public Double apply(Double input) {
return Math.sqrt(input);
}
};
sqrt.apply(4.0);//2.0
这里我们通过实例化一个匿名类的方式来完成了仿函数的定义、初始化和赋值。
注意到仿函数始终不是一个函数,而是一个对象,我们只能调用这个对象的方法来模拟函数调用。
这种 Function 接口的用法和函数式编程中将一个匿名函数赋值给变量的做法类似。当然,更加常见的函数定义方式是显式的声明一个函数然后实现它。
class SqrtFunction implements Function<Double, Double> {
public Double apply(Double input) {
return Math.sqrt(input);
}
}
new SqrtFunction().apply(4.0);//2.0
从接口定义我们可以看出来,Function 接口模拟的函数只能接收一个参数,这不得不说是一个不小的限制。假如我们希望实现一个接收两甚至多个个参数的函数,我们就不得不做一些额外的工作来绕过这个限制。
下面的例子我们实现一个仅接收两个参数的函数。
Function<SimpleEntry<Double, Double>, Double> power
= new Function<SimpleEntry<Double, Double>, Double>() {
public Double apply(SimpleEntry<Double, Double> input) {
return Math.pow(input.getKey(), input.getValue());
}
};
power.apply(new SimpleEntry<Double, Double>(3.0, 2.0));//9.0
最后一个例子是实现一个接收可变参数的函数。由于变长参数实际上是 Java 编译器提供的语法糖,在编译期间会被解语法糖变成对象数组 Object[],而且变长参数无法作为泛型参数,这里直接使用对象数组作为参数。
Function<Double[], Double> sum = new Function<Double[], Double>() {
public Double apply(Double[] input) {
Double result = 0.0;
for (Double element : input) {
result += element;
}
return result;
}
};
sum.apply(new Double[]{3.0, 4.0, 5.1});//12.1
虽然从代码长度来看,使用 Function 接口来定义函数,需要写更多的代码。实际上,大部分的泛型声明和函数覆盖的代码都是由 IDE 自动生成的,手写的代码不过是 apply 的函数体而已。
Functions
Functions 是 Guava 中与 Function 接口配套使用的工具类,为处理实现了 Function 接口的仿函数提供方便。我们一起看看 Functions 是如何让 Function 接口如虎添翼的。
Functions 是一个方法工厂,提供各种返回 Function 实例的方法。如果我们把 Function 视为函数,那么 Functions 的方法就是高阶函数,因为它能够将函数作为它的返回值。
Functions#toStringFunction 返回这样一个函数 ,以对象为入参,以对象的 toString 方法的返回值为返回值。
Functions#identity 返回这样一个函数 ,以对象为入参,返回对象本身。
Functions#constant 返回一个常函数 ,入参就是返回的函数的返回值。
Functions#compose 返回一个复合函数 ,以两个函数为入参,返回这两个函数复合之后的函数。例如我们有函数 和函数 ,复合之后得到复合函数 。
想象一个数据处理程序,我们可以实现一个一个函数,让数据流从函数组成的阀门中间依次流过,最终得到想要的结果。我们可以使用复合函数方法将各个函数组合成流水线,当然也可以使用其他方法。
接下来是 3 组以 for 开头的方法,将其他数据结构或者接口的实例转变成 Function 实例。
Functions#forMap(java.util.Map<K,V>) 以一个映射 map 为入参,返回这样一个函数, 。
Functions#forMap(java.util.Map<K,? extends V>, V) 和上面的方法类似,区别在于这个方法的第二个参数是一个默认值,当映射中不包含入参键的时候,第一个方法返回的函数会抛出异常,而第二个方法返回的函数会返回默认值。
Functions#forPredicate 以 Guava 的谓词实例为入参,返回一个 Function 实例。后续博文会介绍谓词接口 Predicate。
Functions#forSupplier 以 Guava 的 Supplier 实例为入参,返回一个 Function 实例。后续博文会介绍惰性求值接口 Supplier。
源码分析
下面这张图是 Functions 类的结构。
可以看出,Functions 的 8 个公有方法都有对应的内部类作为功能支撑。这 8 个方法的实现大同小异,我们这里选取两个具有代表性的方法进行源码分析。
首先是最简单的 Functions#identity。
public static <E> Function<E, E> identity() {
return (Function<E, E>) IdentityFunction.INSTANCE;
}
// enum singleton pattern
private enum IdentityFunction implements Function<Object, Object> {
INSTANCE;
@Override
@Nullable
public Object apply(@Nullable Object o) {
return o;
}
@Override public String toString() {
return "identity";
}
}
注意到 Functions#identity 其实是个常函数,它返回的函数 可以表示为一个常量或者单例,于是实现中使用了枚举在完成函数定义的同时顺便实现了单例。
接下来是构造复合函数的高阶函数 Functions#compose。
/**
* Returns the composition of two functions. For {@code f: A->B} and {@code g: B->C}, composition
* is defined as the function h such that {@code h(a) == g(f(a))} for each {@code a}.
*
* @param g the second function to apply
* @param f the first function to apply
* @return the composition of {@code f} and {@code g}
* @see <a href="//en.wikipedia.org/wiki/Function_composition">function composition</a>
*/
public static <A, B, C> Function<A, C> compose(Function<B, C> g, Function<A, ? extends B> f) {
return new FunctionComposition<A, B, C>(g, f);
}
Javadoc 里面详细的描述了复合函数的复合方式,参数名的定义也符合数学上对复合函数的常见描述,让人一目了然。最后的 @see 还给出了 WikiPedia 的链接,颇有种旁征博引的感觉。
private static class FunctionComposition<A, B, C> implements Function<A, C>, Serializable {
private final Function<B, C> g;
private final Function<A, ? extends B> f;
public FunctionComposition(Function<B, C> g, Function<A, ? extends B> f) {
this.g = checkNotNull(g);
this.f = checkNotNull(f);
}
@Override
public C apply(@Nullable A a) {
return g.apply(f.apply(a));
}
@Override public boolean equals(@Nullable Object obj) {
if (obj instanceof FunctionComposition) {
FunctionComposition<?, ?, ?> that = (FunctionComposition<?, ?, ?>) obj;
return f.equals(that.f) && g.equals(that.g);
}
return false;
}
@Override public int hashCode() {
return f.hashCode() ^ g.hashCode();
}
@Override public String toString() {
return g + "(" + f + ")";
}
private static final long serialVersionUID = 0;
}
复合函数的支撑类的实现也比较直观,FunctionComposition 类内部持有需要复合的两个 Function 实例,然后在复合函数被调用的时候依次调用持有的两个函数。
一个有趣的地方是关于泛型声明,复合的时候需要声明 3 个类型,函数 的入参类型 A ,函数 的返回值类型(函数 的入参类型) B,函数 的返回值类型 C。初始化 FunctionComposition 的时候,函数 的返回值类型却是 B 或 B 的子类。为什么函数 的返回值类型能够放宽到 B 的子类呢?
原因就是“里氏替换原则”,派生类(子类)对象能够替换其基类(超类)对象被使用,所以函数 的返回值类型如果是 B 的子类,也能够被函数 正确处理。