设计模式学习笔记 - 开源实战三(下):借助Google Guava学习三大编程范式中的函数式编程

概述

现在主流的编程范式主要有三种,面向过程、面向对象和函数式编程。在理论部分,已经介绍了前面两种编程范式。本章再讲讲剩下的编程范式,函数式编程。

函数式编程并非是一个很新的东西,早在 50 年前就已经出现。近几年,函数式编程越来越被人关注,出现了很多新的函数式编程语言,比如 Clogure、Scala、Erlang 等。一些函数式编程语言也加入了很多特性、语法、类库类支持函数式编程,比如 Java、Python、Ruby、JavaScript 等。此外,Google Guava 也有对函数式编程的增强功能。

函数式编程因其编程的特性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以个人觉得,它并不能完全替代更加通用的面向对象编程范式。但是,作为一种补充,它也有很大存在、发展和学习的意义。


到底什么是函数式编程?

函数式编程的英文是 Functional Programming。那到底什么是函数式编程呢?

函数式编程也没有一个严格的官方定义,所以,接下来就从特性上告诉你,什么是函数式编程。

严格来讲,函数式编程中的 "函数" ,并不是指我们编程语言中的 "函数" 概念,而是指数学 "函数" 或者 "表达式"(比如,y=f(x)。不过,在编程实现时,对于数学 "函数" 或者 "表达式",一般习惯性地将它们设计成函数。所以,如果不深究的话,函数式编程中的 "函数" 也可以理解为编程语言中的 "函数"。

每个编程范式都有自己独特的地方,这就是它们会被抽象出来作为一种范式的原因。面向对象编程的最大特点是:以类、对象作为组织代码的单元以及它的四大特性。面向过程编程语言的最大特点是:以函数作为组织代码的单元,数据与方法相分离。

函数式编程最独特的地方在于它的编程思想。函数式编程任务,程序可以用一系列数学函数或表达式的组合来表示。函数式编程是程序面向数学的更底层抽象,将计算过程描述为表达式。不过,这样说你肯定会有疑问,真的可以把任何程序都表示成一组数学表达式吗?

理论上是可以的。但是,并不是所有的程序都适合这么做。函数式编程有它自己适合的应用场景,比如开篇提到的科学计算、数据处理、数据处理、统计分析等。在这些领域,程序往往容易使用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以 用很少的代码就能搞定。但是,对于强业务相关的大型业务系统开发来说,耗费精力地将它抽象成数学表达式,硬要用函数式编程来实现,显然是自讨苦吃。相反,在这种应用场景下,面向对象编程更加合适,写出来的代码更加可读、可维护。

刚刚讲的是函数式编程的编程思想,如果落实到编程实现,函数式编程跟面向过程编程一样,也是以函数作为组织代码的单元。不过,它跟面向过程编程的区别在于,它的函数是无状态的。

何为无状态?简单地讲,函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果是一样的。这实际上就是数学表达式或数学函数的基本要求。下面举个简单的例子来解释下:

java 复制代码
// 有状态函数:执行结果依赖b的值是多少,即便入参相同,多次执行函数,函数的返回值有可能不同,因为b值有可能不同
int b
int increase(int a) {
	return a + b;
}

// 无状态函数:执行结果不依赖任何外部变量,只要入参相同,不管执行多少次,函数的返回值就相同
int increase(int a, int b) {
	return a + b;
}

不同的编程范式之间并不是截然不同的,总有一些相同的编程规则。比如,不管是面向过程、面向对象还是函数式编程,它们都有变量、函数的概念,最顶层都要有 main 函数执行入口,来组装编程单元(类、函数等)。只不过,面向对象的编程单元是类或对象,面向过程的编程单元室函数,函数式编程的编程单元是无状态函数

Java 对函数式编程的支持

前面章节讲过,实现面向对象编程不一定非得使用面向对象编程语言。同理,实现函数式编程也不一定非得使用函数式编程语言。现在,很多面向对象编程语言,也提供了相应的语法、类库来支持函数式编程。

接下来,看下 Java 这种面向对象编程语言,对函数式编程的支持,加深一下你对函数式编程的理解。下面是一段非常典型地 Java 函数式编程的代码。

java 复制代码
public class FPDemo {
    public static void main(String[] args) {
        Optional<Integer> result = Stream.of("f", "a", "hello")
                .map(s -> s.length())
                .filter(l -> l <= 3)
                .max((o1, o2) -> o1 - o2);
        System.out.println(result.get()); // 输出2
    }
}

这段代码的作用是从一组字符串数组中,过滤出长度小于等于 3 的字符串,并且求得这其中的最大长度。

如果你不了解 Java 函数式编程的语法,看了上面的代码或许会有些懵,主要的原因是 Java 为函数式编程引入了三个新的语法概念:StreamLambda 表达式函数接口(Function Interface)。

  • Stream 类用来支持通过 "." 级联多个函数操作的代码编写方式;
  • 引入 Lambda 表达式的作用是简化代码编写;
  • 函数接口的作用是可以把函数包裹成函数接口,来实现把函数当做参数一样来使用(Java 不像 C 一样支持函数指针,可以把函数当做参数来使用)。

首先,看下 Stream 类

假设要计算这样一个表达式:(3-1)*2 + 5。如果按照普通的函数调用方式写出来,就是下面这个样子:

java 复制代码
add(multiply(subtract(3, 1), 2), 5);

这样看起来,代码会比较难理解,换个更易懂的写法:

java 复制代码
subtract(3, 1).multiply(2).add(5);

在 Java 中, "." 用来表示某个对象的方法。为了支持上面这种级联调用方式,我们让每个函数都返回一个通用的类 型:Stream 类对象。在 Stream 类上的操作有两种:中间操作和终止操作。中间操作仍返回 Stream 类对象,而终止操作返回的是确定的值结果。

再看下前面的例子。我们对代码做了注释解释,如下所示。其中,mapfilter 是中间操作,返回 Stream 类对象,可以继续级联其他操作;max 是终止操作,返回的不是 Stream 类对象,无法继续往下级联处理了。

java 复制代码
public class FPDemo {
    public static void main(String[] args) {
        Optional<Integer> result = Stream.of("f", "a", "hello") // of返回Stream<String>对象
                .map(s -> s.length()) // map返回Stream<Integer>对象
                .filter(l -> l <= 3) // filter返回Stream<Integer>对象
                .max((o1, o2) -> o1 - o2); // max 终止操作:返回Option<Integer>
        System.out.println(result.get()); // 输出2
    }
}

其次,再看下 Lambda 表达式

Java 引入 Lambda 表达式的主要作用是简化代码编写。实际上,我们也可以不用 Lambda 表达式来书写书中的例子。我们拿其中的 map 函数来举例说明下。

java 复制代码
// Stream中map的定义:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    // 省略其他函数...
}

// Stream中map的使用方法:
Stream.of("fo", "far", "hello").map(new Function<String, Integer>() {
   @Override
    public Integer apply(String s) {
        return s.length();
    }
});

// 用Lambda表达式简化后的写法:
Stream.of("fo", "far", "hello").map(s -> s.length());

Lambda 表达式语法不是学习的重点,这里只是稍微介绍下。

Lambda 表达式包含三部分:输入、函数体、输出。表示出来就是下面这个 样子:

java 复制代码
(a, b) -> { 语句1; 语句2; ...; return 输出;} // a,b是输入参数

实际上,Lambda 表达式的写法非常灵活。刚刚给出的是标准写法,还有很多简化写法。比如,如果输入参数只有一个,可以省略 (),直接写成 a ->{...};如果没有入参,可以直接将输入和箭头都省略只保留函数体;如果函数体只有一个语句,可以将 {} 省略掉; 如果函数没有返回值, return 语句就可以不用写了。

如果把之前的例子中的 Lambda 表达式,全部替换为函数接口的实现方式,就是下面这样子的。代码是不是多了很多?

java 复制代码
Optional<Integer> result = Stream.of("f", "a", "hello")
        .map(s -> s.length())
        .filter(l -> l <= 3)
        .max((o1, o2) -> o1 - o2);

// 还原为函数接口的实现方式
Optional<Integer> result2 = Stream.of("fo", "far", "hello")
	.map(new Function<String, Integer>() {
	    @Override
	    public Integer apply(String s) {
	        return s.length();
	    }
	})
	.filter(new Predicate<Integer>() {
	    @Override
	    public boolean test(Integer integer) {
	        return integer <= 3;
	    }
	})
	.max(new Comparator<Integer>() {
	    @Override
	    public int compare(Integer o1, Integer o2) {
	        return o1 - o2;
	    }
	});

最后,看下函数接口

实际上,上面一段代码中的 FunctionPredicateComparator 都是函数接口。我们知道,C 语言支持函数指针,它可以把函数直接当变量来使用。但是,Java 没有函数指针这样的语法。所以,它通过接口函数,将函数包裹在接口中,当做变量来使用。

实际上,函数接口就是接口。不过,它有自己特别的地方,那就是要求只包含一个未实现的方法。因为,只有这样,Lambda 表达式才能明确知道匹配的是哪个接口。如果有两个为实现的方式,并且接口入参、返回值都一样,那 Java 在翻译 Lambda 表达式时,就不知道表达式对应哪个方法了。

我们把 Java 提供的 FunctionPredicate 这两个函数接口的源码,摘抄过来贴到了下面,你可以看下。

java 复制代码
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

以上讲的就是 Java 对函数式编程的语法支持。

Guava 对函数式编程的增强

如果你是 Google Guava 的设计者,对于 Java 函数式编程,Google Guava 还能做些什么呢?

Google Guava 并没有提供太多函数式编程的支持,仅封装了几个遍历集合操作的接口,代码如下所示:

java 复制代码
Iterables.transform(Iterable, Function);
Iterables.transform(Iterator, Function);
Collections.transform(Collection, Function);
List.transform(List, Function);
Maps.transform(Map, Function);
Multimaps.transform(Multimap, Function);
...

Iterables.filter(Iterable, Predicate);
Iterators.filter(Iterable, Predicate);
Collections2.filter(Collection, Predicate);
...

从 Google 的 Wiki 中,我们发现,Google 对于函数式编程的使用还是很谨慎的,认为过度使用函数式编程,会导致代码的可读性变差,强调不滥用。所以,在函数式编程方面,Google Guava 并没有提供太多的支持。

之所以对遍历集合操作做了优化,主要是因为函数式编程一个重要的应用场景就是遍历集合。如果不适用函数式编程,我们只能 for 循环,一个一个的处理集合中的属性。使用函数式编程,可以大大简化遍历集合操作的代码编写,一行代码就能搞定,而且在可读性方面也没有太大的损失。

总结

本章,讲了一下三大编程范式中的最后一个,函数式编程。尽管越来越多的编程语言开始支持函数式编程,但我个人觉得,它只能是其他编程语范式的补充,用在一些特殊的领域发挥它的特殊作用,没法完全替代面向对象、面向过程编程范式。

关于什么是函数式编程,实际上不是很好理解。函数式编程中的 "函数",并不是只我们编程语言中的 "函数" 概念,而是数学中的 "函数" 或者 "表达式" 概念。函数式编程认为,程序可以用一系列数学函数或者表达式的组合来表示。

具体到编程实现,函数式编程以无状态函数作为组织代码的单元。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。

具体到 Java 语言,它提供了三个语法机制来支持函数式编程。它们分别是 Stream 类、Lambda 表达式和函数接口。Google Guava 对函数式编程的一个重要应用场景,遍历集合,做了优化,但并没有太多的支持,并且强调,不要为了节省代码行数,滥用函数式编程,导致代码可读性变差。

相关推荐
Oberon11 天前
从零开始的函数式编程(2) —— Church Boolean 编码
数学·函数式编程·λ演算
桦说编程20 天前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程
桦说编程24 天前
如何安全发布 CompletableFuture ?Java9新增方法分析
java·性能优化·函数式编程·并发编程
桦说编程1 个月前
【异步编程实战】如何实现超时功能(以CompletableFuture为例)
java·性能优化·函数式编程·并发编程
鱼樱前端1 个月前
Vue3之ref 实现源码深度解读
vue.js·前端框架·函数式编程
RJiazhen2 个月前
前端项目中的函数式编程初步实践
前端·函数式编程
再思即可3 个月前
sicp每日一题[2.77]
算法·lisp·函数式编程·sicp·scheme
桦说编程3 个月前
把 CompletableFuture 当做 monad 使用的潜在问题与改进
后端·设计模式·函数式编程
蜗牛快跑2133 个月前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
大福是小强4 个月前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数