美文网首页IT@程序员猿媛程序员
用 JS 代码解释 Java Stream

用 JS 代码解释 Java Stream

作者: 刀哥说Java | 来源:发表于2019-04-05 14:52 被阅读5次

引言

前不久,公司后端同事找到我,邀请我在月会上分享函数式编程,我说你还是另请高明吧…… 我也不是谦虚,我一个前端页面仔,怎么去给以 Java 后端开发为主的技术部讲函数式编程呢?但是同事说你还是试试吧。然后我就去先试着准备下。

由于我最近在学函数式领域建模(Functional Domain Modeling),一开始我想讲下 Scala,然后我找到了 Functional and Reactive Domain Modeling 这本书。但是转念一想,我都没写过后端,对后端的业务场景根本不了解,看本书就去讲,有些不尊重听众智商了。最后我打算在 Java 里找些接地气的内容来分享,然后我就去学了 Java,发现了 Java 里面有 Stream 这么好的函数式一级语言支持。

最终经过谨慎考虑后,我还是没有去分享。我没有业务上的理解,仅仅去讲些语法特性和奇技淫巧,有些班门弄斧了。我在学习 Stream 的过程中有一些发现,现在把这些发现分享出来。

本文代码在我上一篇文章中出现过,但这次解释更详细。

JavaScript 更适合用来学习一些 FP 概念

本文无意抛出 JavaScript 和 Java 谁比谁好这种无聊的论断,我仅想指出,JS 这种弱类型语言可以避免一些语法噪音,让初学者在学习一些 FP 概念时,快速直抵核心,理解本质。

在学 Java Stream 时,要先学 lambda expression, method reference, 然后学 Interface 和 Functional Interface, 学这些仅仅为了支持 lambda 传参。而用 JS 的话,函数顺手就写,顺手就传,不用那么多准备仪式。

再次声明一下,Java 这种面向对象特性有其强大之处。这里仅仅指出在学习理解一些 FP 概念上,JS 更简单灵活。

理解 Stream

Stream 可以拆解成三个部分:

如上图,Stream 可分为数据源,管道数据操作,数据消费三个部分。源数据生成后,流经管道,最终在管道终端被消费。最终的消费可能是数据流被聚合成新的数据集,也可能是逐个执行副作用(forEach)。

下面用 JS 代码解释这三个部分。

数据源

数据源可以理解成是一个 generator 函数被反复执行,生成数据流。什么是 generator 呢?最简单的 generator 可以是这样:

const getRandNum = () => Math.floor(Math.random() * 100);

复制代码

每次执行 getRandNum 它都随机生成一个 0 到 100 的整数,它满足我们对 generator 行为的期待。可以看出 generator 其实就是一个动态生成数据的行为,那如果数据源是静态数据集,怎么得到这种动态 generator 呢?很简单:

function getGeneratorFromList(list) {

  let index = 0;

  return function generate() {

    if (index < list.length) {

      const result = list[index];

      index += 1;

      return result;

    }

  };

}

// 例子:

const generate = getGeneratorFromList([1, 2, 3]);

generate(); // 1

generate(); // 2

generate(); // 3

generate(); // undefined

给 getGeneratorFromList 传个数组,它会返回一个 generator,这个 generator 每被执行一次都吐出传入数组当前遍历到的元素。这里只考虑数组,其它情况很容易扩展。

数据源部分就讲完了。其实很简单。

管道组合

数据源生成后,就进入管道了。管道里对原 generator 进行各种转换,组合生成符合期待的 generator。注意上一句的描述,管道里仅仅是基于原 generator 函数生成新的 generator 函数,计算行为并不会被触发。

我们给管道里塞入一些高阶操作函数,这些高阶操作函数接受前一个函数吐出的 generator,返回加入了新行为的 generator。我们先来定义一个 map:

function map(mapping) {

  return function(generate) {

    return function mappedGenerator() {

      const value = generate();

      if (value !== undefined) {

        return mapping(value);

      }

    };

  };

}

复制代码

map 函数连续返回了两个函数。这里先简要解释下为什么这么做,可能会比较难懂,等你看了后面的代码再回过头来看会更容易理解些。当用户调用 map 并将结果传入管道时,map 返回了第二层函数。当管道组合被触发时,第二层函数被执行,最终 generator 函数被返回,然后返回的 generator 被传给管道里的下一个高阶操作函数。

再来定义一个 filter:

function filter(predicate) {

  return function(generate) {

    return function filteredGenerator() {

      const value = generate();

      if (value !== undefined) {

        if (predicate(value)) {

          return value;

        }

        return filteredGenerator();

      }

    };

  };

}

当判断条件不满足时,generator 会跳过当前值,持续递归,直到遍历到符合条件的值才将值返回出去。

map 和 filter 是最重要的高阶操作函数,其它的操作函数就不展开解释了。

在提供了高阶操作函数之后,我们要提供一个函数将这些高阶函数组合起来。

function Stream(source) {

  let initialGenerator;

  if (Array.isArray(source)) {

    initGenerator = getGeneratorFromList(source);

  } else {

    initialGenerator = source;

  }

  function pipe(...operators) {

    return operators.reduce((prev, current) => current(prev), initialGenerator);

  }

  return { pipe };

}

本文就只考虑数据源是 generator 函数和数组两个情况了。这种考虑当然是不严谨的,但足以解释 Stream 的实现。

pipe 函数将操作函数从左往右依次执行,并将上一个函数执行的结果传给下一个函数。如此则完成了管道组合操作。

数据消费

前面两步完成后,就剩下如何将动态的 generator 转换成静态的数据集了(forEach 执行副作用本文就不考虑了)。这一步也比较简单:

function toList(generate) {

  const arr = [];

  let value = generate();

  while (value !== undefined) {

    arr.push(value);

    value = generate();

  }

  return arr;

}

这里就只展示生成数组了,其它数据类型读者可自行扩展。

至此,完整的 Stream 就实现了。当然,操作符支持上还不完整,但你能明白我的意思。

再定义一个 take,然后测试下:

function take(n) {

  return function(generate) {

    let count = 0;

    return function() {

      if (count < n) {

        count += 1;

        return generate();

      }

    };

  };

}

Stream(getRandNum).pipe(

  filter(x => x % 2 === 1),

  take(10),

  toList

);

// => 10 个随机奇数

注意,getRandNum 永远都不会返回 undefined,那为什么 toList 没有进入死循环?这是因为 take 给原 generator 加入了新的行为,让它只能返回 10 个有效值。这也是惰性求值的魅力。

更多高阶操作符如下:

function skipWhile(predicate) {

  return function(generate) {

    let startTaking = false;

    return function skippedGenerator() {

      const value = generate();

      if (value !== undefined) {

        if (startTaking) {

          return value;

        } else if (!predicate(value)) {

          startTaking = true;

          return value;

        }

        return skippedGenerator();

      }

    };

  };

}

function takeUntil(predicate) {

  return function(generate) {

    return function() {

      const value = generate();

      if (value !== undefined) {

        if (predicate(value)) {

          return value;

        }

      }

    };

  };

}

后记

Java 里面的 Stream 底层实现我并没有学习,我只学了下 API 用法。但是从 Stream API 的行为特性来推断,其底层实现应该和本文展示的 JS 实现思想是相通的。

相关文章

  • 用 JS 代码解释 Java Stream

    引言 前不久,公司后端同事找到我,邀请我在月会上分享函数式编程,我说你还是另请高明吧…… 我也不是谦虚,我一个前端...

  • Java流操作总结

    Java流(Stream)操作自Java 8引入,通过Stream操作可以简化代码编写,提高代码执行效率。流整体操...

  • Stream

    使用 Stream API 高逼格 优化 Java 代码 Java8 Stream流中的 collect() 别再...

  • android 获取WebView loadData后的高度

    网页高度用 java 调用 js 获取 ,实际是用 js 代码进行获取。view高度:

  • Java Stream 源码分析

    前言 Java 8 的 Stream 使得代码更加简洁易懂,本篇文章深入分析 Java Stream 的工作原理,...

  • Java Stream 源码分析

    前言 Java 8 的 Stream 使得代码更加简洁易懂,本篇文章深入分析 Java Stream 的工作原理,...

  • js预解析(面试哦)

    js 代码通过 js 解释器(js 引擎) 来执行的 js 解释器 来执行js 代码分为两步: 首先预解析 再 ...

  • 怎样在浏览器上运行Java代码

    今天忽然想通了如何在浏览器上解释运行java代码的问题。 以前考虑在js上开发一个java解释器,最大的困难是如何...

  • 怎样在浏览器上运行Java代码

    今天忽然想通了如何在浏览器上解释运行java代码的问题。 以前考虑在js上开发一个java解释器,最大的困难是如何...

  • Java使用Optional与Stream来取代if判空逻辑(J

    Java使用Optional与Stream来取代if判空逻辑(JDK8以上) 通过本文你可以用非常简短的代码替代业...

网友评论

    本文标题:用 JS 代码解释 Java Stream

    本文链接:https://www.haomeiwen.com/subject/qpahiqtx.html