Java Python JS 流式编程(链式编程)

Java \ Python \ JS 流式编程(链式编程)

前言

我们学习编程通常是从命令式编程起步的。比如说计算1~100的平方和,我们会这样写:

let result = 0
for (let i = 1; i <= 100; i++) {
    result += i * i
}

这样写的好处是逻辑清楚,每一条语句的作用显而易见。但是随着代码量越来越多,其缺陷也渐渐凸显出来。

  • 冗余代码过多。比如求和、统计这样的功能,很多地方都会用到,然而又不得不重复写。
  • 多线程下的并发安全。命令式编程在多线程环境下需要特别注意并发安全,因为命令式编程多为可变对象。
  • 代码不够优雅。一段代码中有很多无关紧要的变量,如临时的result,循环用的i,都是不必要的,会影响代码的可读性。

此时,便引出了另一种编程方法: 流式编程。流式编程和链式编程属于同一种东西,所以本文统一称为流式编程。本文不会深入讲解流式编程的底层实现,而是让大家能快速理解流式编程的思想,并上手写出简洁的代码。

流式编程

什么是流式编程?

一句话描述流式编程: 基于集合变换的编程。

那么什么是集合变换?又该怎么变换呢?

基本思想:集合变换

先来看一个例子: 找出一个数组中大于100的数。如果用面向过程的思想,我们会怎么做呢?

  1. 遍历数组
  2. 如果该数大于100,则把数追加到新的数组
const raw = [1, 2, 5, 100, 121]
let result = []
for (const e of raw) {
    if (e > 100) {
        result.push(e)
    }
}

这是我们从学习编程之初就形成的思维,现在我们从更高的抽象角度理解这个问题:

raw --过滤--> result从原始数据到结果,实际上就是对原始集合进行了过滤的操作,留下大于100的数。因此,上面的代码可以这样改写:

const raw = [1, 2, 5, 100, 121]
const result = raw.filter(e => e > 100)

是不是变得十分简洁?其中的filter是过滤函数,es6数组自带的操作。不光是JS,python\java等很多语言都集成了流式编程的基本功能,我们可以很方便地实现各种高级功能。

高阶函数: map, reduce, filter

这三个操作是流式编程里面用的最广泛的,还有groupBy、collect等操作留给读者自行研究

  • map, 元素映射。其作用是将原集合的每个元素,通过一个操作映射到目标集合。举个例子,求一个数组的平方,用map操作就是这样的:

    const raw = [1, 2, 3]
    const res = raw.map(e => e * e)  // [1, 4, 9]
    

    其底层实现是遍历每个元素,然后追加到新的数组里。

  • reduce, 元素合并。其作用是迭代地对两个元素进行合并操作。比如求一个数组的和,便可以这样写:

    const raw = [1, 2, 3]
    const sum = raw.reduce((x, y) => x + y) // 6
    

    其底层实现是遍历每个元素,两两合并,然后将结果与下一个元素进行合并。

    如: 1. temp = x[0] + x[1] 2. temp = temp + x[2] 3. temp = temp + x[3] …

  • filter,元素过滤。其作用是将满足条件的元素留存到另一个集合,比如求数组中小于100的数:

    const raw = [1, 2, 3]
    const res = raw.filter(e => e < 100)
    

    其底层操作是遍历+if判断

链式编程,流动起来

流式编程还有另一个特点,那就是流函数执行之后,返回的也是一个流。这样的话,就可以完成更复杂的工作了。比如求数组中超过100,且其平方小于50000的平方和。用命令式写法咋写呢?

const raw = [1, 2, 4, 100, 122, 444]
let res = 0
for (const e of raw) {
    if (e > 100 && e * e < 50000) {
        res += e * e
    }
}

虽然代码不多,但是各种判断混合在一起,影响整个代码逻辑的表达

那么用集合变换的思想,不难抽象为以下三步:

  1. 超过100的数 -> 过滤
  2. 平方 -> 映射
  3. 平方小于50000 -> 过滤
  4. 和 -> 合并

于是,用一句话便可以完成这个功能:

const raw = [1, 2, 100, 123]
const sum = (x, y) => x * y // 求和函数
// [raw] --> [大于100] --> [平方] --> [平方小于50000] --> [和]
const res = raw.map(e => e > 100).map(e => e * e).filter(e => e < 50000).reduce(sum)

优势

流式编程有几大优势

  1. 逻辑清晰,代码优雅 这是流式编程的最大优点,因为其每一步都是描述形式的,仅从所用的函数便能知道其作用。比如看到filter就知道是在做过滤。
  2. 不可变集合,更安全 流式编程,乃至函数式编程,都提倡不可变集合(本文中大量使用的const),这样每一步操作都是无副作用的,调试起来更简单。
  3. 更方便并行 有了2中的不可变集合的特点,流式编程可以很方便地实现数据并行,而不用考虑并发安全(不可变数据是并发安全的)。Java里的parallelStream便是并行流的实现。

缺点

有优势便必然有劣势

  1. 不是万能的。 没有一种技术或者方法是万能的,流式编程也一样。其优势主要在于数据处理、变换,而对于抽象层度不高的场合,并不适用。所以最佳实践是,在需要数据的变换时,用流式编程,其他场合用命令式编程。
  2. 开销较大。 因为使用了不可变集合,则意味着运行过程中会产生更多的中间对象,增加GC的负担。

各个语言的流式编程实现

  • Java。 java8开始有了Stream和lambda表达式,用Stream和ParallelStream(并行版本)与lambda表达式(匿名函数)可以很方便地实现流式编程。

    https://www.runoob.com/java/java8-streams.html

  • Python。python 原生的 列表推导式可以实现集合变换,但无法实现链式操作,因此不是很高效。我比较推荐 PyFunctional这个库,其包含了 map、reduce、filter这些基本的操作,还提供了 groupBy(聚合) 这类的高级函数。

    https://scalafunctional.readthedocs.io/en/master/functional.html#streams-api

  • JS。js的Array自带map、reduce、fiter等函数,groupBy等需要自己扩展

    https://www.runoob.com/jsref/jsref-obj-array.html

你可能感兴趣的:(Java Python JS 流式编程(链式编程))