Guava 是个风火轮之函数式编程(1)

简介:

前言

函数式编程是一种历久弥新的编程范式,比起命令式编程,它更加关注程序的执行结果而不是执行过程。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 返回这样一个函数  f ( x ) = x . t o S t r i n g ( ) ,以对象为入参,以对象的 toString 方法的返回值为返回值。

Functions#identity 返回这样一个函数  f ( x ) = x ,以对象为入参,返回对象本身。

Functions#constant 返回一个常函数  f ( x ) = a ,入参就是返回的函数的返回值。

Functions#compose 返回一个复合函数  h ( x ) = g ( f ( x ) ) ,以两个函数为入参,返回这两个函数复合之后的函数。例如我们有函数  f : X Y  和函数  g : Y Z ,复合之后得到复合函数  g f : X Z

想象一个数据处理程序,我们可以实现一个一个函数,让数据流从函数组成的阀门中间依次流过,最终得到想要的结果。我们可以使用复合函数方法将各个函数组合成流水线,当然也可以使用其他方法。

接下来是 3 组以 for 开头的方法,将其他数据结构或者接口的实例转变成 Function 实例。

Functions#forMap(java.util.Map<K,V>) 以一个映射 map 为入参,返回这样一个函数, f ( x ) = m a p . g e t ( x )

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 其实是个常函数,它返回的函数  f ( x ) = x  可以表示为一个常量或者单例,于是实现中使用了枚举在完成函数定义的同时顺便实现了单例。

接下来是构造复合函数的高阶函数 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 个类型,函数  f  的入参类型 A ,函数  f  的返回值类型(函数  g  的入参类型) B,函数  g  的返回值类型 C。初始化 FunctionComposition 的时候,函数  f  的返回值类型却是 B 或 B 的子类。为什么函数  f  的返回值类型能够放宽到 B 的子类呢?

原因就是“里氏替换原则”,派生类(子类)对象能够替换其基类(超类)对象被使用,所以函数  f  的返回值类型如果是 B 的子类,也能够被函数  g  正确处理。


目录
相关文章
|
2月前
|
Java 数据库
震惊!Guava Throwables 类如魔法棒般神奇,让 Java 异常处理华丽变身!
【8月更文挑战第29天】在 Java 开发中,异常处理至关重要,而 Guava 库中的 `Throwables` 类则提供了强大的异常处理工具。它包含了一系列静态方法,如 `propagateIfInstanceOf` 和 `propagateIfPossible`,可以帮助我们有条件地传播异常。此外,`getRootCause` 方法可以深入分析异常的根本原因,有助于快速定位问题所在。无论是构建大型分布式系统还是电商平台,`Throwables` 类都能显著提升异常处理的效率和准确性,使我们的程序更加稳定和可靠。
32 1
|
5月前
|
XML Java 测试技术
Java异常处理神器:Guava Throwables类概念与实战
【4月更文挑战第29天】在Java开发中,异常处理是保证程序稳定性和可靠性的关键。Google的Guava库提供了一个强大的工具类Throwables,用于简化和增强异常处理。本篇博客将探讨Throwables类的核心功能及其在实战中的应用。
92 2
|
SQL JavaScript 前端开发
函数式编程,真香
最开始接触函数式编程的时候是在小米工作的时候,那个时候看老大以前写的代码各种 compose,然后一些 ramda 的一些工具函数,看着很吃力,然后极力吐槽函数式编程,现在回想起来,那个时候的自己真的是见识短浅,只想说,'真香'。
175 0
函数式编程,真香
|
安全 Java Android开发
重学Kotlin之那些你没注意到的细节
Kotlin中的一些关键字
176 0
|
消息中间件 算法 前端开发
Kotlin可能带来的一个深坑,实战篇
Kotlin可能带来的一个深坑,实战篇
Kotlin可能带来的一个深坑,实战篇
|
XML JSON 缓存
「造个轮子」——cicada 源码分析(上)
本文就目前的 v1.0.1 版本来一起分析分析。
|
设计模式 JSON 缓存
「造个轮子」——cicada 源码分析(下)
本文就目前的 v1.0.1 版本来一起分析分析。
|
安全 Java
老司机阿粉带你玩转 Guava 集合类(二)
日常开发中,阿粉经常需要用到 Java 提供集合类完成各种需求。Java 集合类虽然非常强大实用,但是提供功能还是有点薄弱。 举个例子,阿粉最近接到一个需求,从输入一个文档中,统计一个关键词出现的次数。代码如下
老司机阿粉带你玩转 Guava 集合类(二)
|
缓存 Java 开发者
老司机阿粉带你玩转 Guava 集合类(一)
日常开发中,阿粉经常需要用到 Java 提供集合类完成各种需求。Java 集合类虽然非常强大实用,但是提供功能还是有点薄弱。 举个例子,阿粉最近接到一个需求,从输入一个文档中,统计一个关键词出现的次数。代码如下:
老司机阿粉带你玩转 Guava 集合类(一)
|
Java 编译器 BI
函数式编程,这样学就废了
大家好,上次指北君给大家开启了函数式接口的介绍,今天,指北君将在第一篇JDK源码解析——深入函数式接口(应用篇一)基础上继续为大家解读函数式接口涉及到的知识点。本篇文章为函数接口的应用篇二,将会为各位小伙伴详细介绍“@FunctionInterface”注解,java.util.function包中所有接口。
函数式编程,这样学就废了