写点什么

关于 Java&JavaScript 中 (伪)Stream 式 API 对比的一些笔记

作者:山河已无恙
  • 2022 年 7 月 18 日
  • 本文字数:9633 字

    阅读完需:约 32 分钟

写在前面

  • 前些时日开发遇到,想着把这些对比总结下

  • 博文内容包括:Stream 相关概念简述 Java 和 JavaScript 的 Stream 式 API 对比 Demo

  • 食用方式博文适合会一点前端的 Java 后端 &会一点 Java 后端的前端需要了解 Java&JavaScript 基础知识

  • 理解不足小伙伴帮忙指正

追求轻微痛感,掌控快感释放,先做困难的事情,降低奖励期待,控制欲望,延迟消费多巴胺


什么是流(Stream)

关于Stream, 在 Java 中我们叫,但是在 JavaScript 中,好像没有这种叫,也没有StreamAPI,我么姑且称为伪流,JS 一般把参与流处理的函数称为高价函数,比如特殊的柯里化之类,Java 中则是通过函数式接口实现,

其实一个编译型语言,一个解释型语言没有什么可比性,这里只是感觉行为有写类似放到一起比较记忆。而且通过链式调用,可读性很高,JS 里我们主要讨论 Array 的伪流处理。Set 和 Map 的 API 相对较少,这里不讨论,为了方便,不管是 Java 还是 JavaScript,数据处理我们都称为流或者 Stream 处理

这里的高阶函数,即满足下面两个条件:

  1. 函数作为参数被传递:比如回调函数

  2. 函数作为返回值输出:让函数返回可执行函数,因为运算过程是可以延续的

这里讲Stream,即想表达从一个数据源生成一个想要的元素序列的过程。这个过程中,会经历一些数据处理的操作,我们称之为流(Stream)处理

Stream与传统的数据处理最大的不同在于其 内部迭代,与使用迭代器显式迭代不同,Stream 的迭代操作是在背后进行的。数据处理的行为大都遵循函数式编程的范式,通过匿名函数的方式实现行为参数化,利用Lambad表达式实现。

但是Java的流和JavaScript伪流不同的,Java 的 Stream 是在概念上固定的数据结构(你不能添加或删除元素),JavaScript 中的 Stream 是可以对原始数据源处理的。但是 Java 的 Stream 可以利用多核支持像流水线一样并行处理.

Java中通过parallelStream可以获得一个并行处理的Stream

// 顺序进行List<Apple> listStream = list.stream()        .filter((Apple a) -> a.getWeight() >20 || "green".equals(a.getColor()))        .collect(Collectors.toList());//并行进行List<Apple> listStreamc = list.parallelStream()        .filter((Apple a) -> a.getWeight() >20 || "green".equals(a.getColor()))        .collect(Collectors.toList());
复制代码

JS 可以在流处理的回调函数上可以传递一个当前处理的数据源

let colors = ["red", "blue", "grey"];
colors.forEach((item, index, arr) ==> {    if(item === "red") {        arr.splice(index, 1);    }});
复制代码

一般我们把可以连接起来的Stream操作称为中间操作关闭Stream的操作称为我们称为终端操作

  • 中间操作:一般都可以合并起来,在终端操作时一次性全部处理

  • 终端操作:会从流的流水线生成结果。其结果是任何不是流的值

总而言之,流的使用一般包括三件事:

  • 一个数据源(如数组集合)来执行一个查询

  • 一个中间操作链,形成一条流的流水线

  • 一个终端操作,执行流水线,并能生成结果

关于流操作,有无状态和有状态之分 :

  • 诸如 map或filter 等操作会从输入流中获取每一个元素,并在输出流中得到 0 或 1 个结果。 这些操作一般都是无状态的:它们没有内部状态,称为无状态操作

  • 诸如sort或distinct,reduce等操作一开始都和 filter 和 map 差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题。我们把这些操作叫作有状态操作

中间操作

终端操作

Java 和 JavaScript 的 Stream Demo

Java 和 Node 版本

java version "1.8.0_251"Java(TM) SE Runtime Environment (build 1.8.0_251-b08)Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
复制代码


Welcome to Node.js v16.15.0.Type ".help" for more information.>
复制代码

通过 Demo 来看下 Java 和 JavaScript 的 Stream

filter 筛选

filter 用布尔值筛选,。该操作会接受一个谓词(一个返回 boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

Java

Stream<T> filter(Predicate<? super T> predicate); boolean test(T t);

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream().filter( i -> i % 2 == 0)             .forEach(System.out::print);        // 1244
复制代码

JS

arr.filter(callback(element[, index[, array]])[, thisArg])

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },              { name: "毋固", value: "202203" },{ name: "毋我", value: "202204" }]
users.filter(o => +o.value === 202201 ).forEach(o =>console.log('out :%s',o))//out :{ name: '毋意', value: '202201' }
复制代码

map 映射

对流中每一个元素应用函数:流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映 射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。

java

<R> Stream<R> map(Function<? super T, ? extends R> mapper); R apply(T t);

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream().map(o -> o+1 ).forEach(System.out::println);
======134565

复制代码

JS

arr.map(function callback(currentValue[, index[, array]]) {}[, thisArg])

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },              { name: "毋固", value: "202203" },{ name: "毋我", value: "202204" }]             users.map( o => o.name ).forEach(o =>console.log('out :%s',o))
===========out :毋意out :毋必out :毋固out :毋我
复制代码

flatMap 扁平化

流的扁平化,对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]

java

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

R apply(T t);

List<String> strings = Arrays.asList("Hello","World");strings.stream().map(o -> o.split(""))        .flatMap(Arrays::stream)        .forEach(System.out::println);====HelloWorld        
复制代码

JS

arr.flatMap(function callback(currentValue[, index[, array]]) {}[, thisArg])

let string = ["Hello","World"]string.flatMap( o => o.split("")).forEach(o =>console.log('out :%s',o))
=====out :Hout :eout :lout :lout :oout :Wout :oout :rout :lout :d
复制代码

当然这里JS提供了flat方法可以默认展开数组,flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

[1, 2, [3, [4, 5]]].flat()// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)// [1, 2, 3, 4, 5]
复制代码

slice|limit 截断

截断流:该方法会返回一个不超过给定长度的流。所需的长度作为参数传递 给limit。如果流是有序的,则多会返回前n个元素

通过截断流我们可以看到Java的JavaScript在Stream上本质的不同,Java 通过 Stream 对象本身OP_MASK属性来截断,而 JS 没有实际意义上的 Stream 对象, 但是可以通过filter结合index来完成,或者使用slice

java

Stream<T> limit(long maxSize);

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream().limit(2).forEach(System.out::println);=====123
复制代码

JS

JS 的截断处理可以使用slice,或者通过filter结合index来完成

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },              { name: "毋固", value: "202203" },{ name: "毋我", value: "202204" }]   users.slice(0,2).forEach(o =>console.log('out :%s',o))
======================================out :{ name: '毋意', value: '202201' }out :{ name: '毋必', value: '202202' }
users.filter((_, i) => i <= 1).forEach(o => console.log('out :%s', o))============out :{ name: '毋意', value: '202201' }out :{ name: '毋必', value: '202202' }
复制代码

sort|sorted 排序

排序,这个不多讲,

java

Stream<T> sorted(Comparator<? super T> comparator); int compare(T o1, T o2);

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream()        .sorted( (o1,o2) -> o1 > o2 ? 1 : (o1 < o2 ? -1 : 0 ))        .forEach(System.out::println);===========344512
复制代码

JS

arr.sort([compareFunction])

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },              { name: "毋固", value: "202203" },{ name: "毋我", value: "202204" }]  users.map(o => { return { name: o.name, value: +o.value } })     .sort((o1, o2) => o1.value > o2.value ? -1 : (o1.value < o2.value ? 1 : 0))     .forEach(o => console.log(o))=================================={ name: '毋我', value: 202204 }{ name: '毋固', value: 202203 }{ name: '毋必', value: 202202 }{ name: '毋意', value: 202201 }     
复制代码

distinct 去重

筛选不同的元素:java 流支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的 hashCode和equals方法实现)的流

java

Stream<T> distinct();

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream().distinct().forEach(System.out::println);=========12345
复制代码

JS

distinct 是 Stream 本身的方法,JS 没有类似的代替,不过可以转化为 Set 处理

let numbers = [2,3,4,3,5,2]Array.from(new Set(numbers)).forEach(o => console.log(o))
复制代码

Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为 NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。set 中两个对象总是不相等的。

skip 跳过

跳过元素:返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。请注意,limit(n)和skip(n)是互补的

java

Stream<T> skip(long n);

List<Integer> list  = Arrays.asList(12, 3, 4, 5, 4);list.stream().skip(2).forEach(System.out::println);==================454

复制代码

JS

Js 中可以通过slice方法来实现

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },              { name: "毋固", value: "202203" },{ name: "毋我", value: "202204" }] users.slice(2).forEach(o => console.log(o))             ========={ name: '毋固', value: '202203' }{ name: '毋我', value: '202204' }
复制代码

group/groupToMap|groupingBy 分组

分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值

java

Java 的分组通过 Stream API 的collect方法传递Collector静态方法groupingBy,该方法传递了一个Function(以方法引用的形式)我们把这个 Function 叫作分类函数,因为它用来把流中的元素分成不同的组。

<R, A> R collect(Collector<? super T, A, R> collector);

public static <T, K, A, D>    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,                                      Collector<? super T, A, D> downstream) {    return groupingBy(classifier, HashMap::new, downstream);}
复制代码


@FunctionalInterfacepublic interface Function<T, R> {      R apply(T t);}
复制代码

这块涉及的 API 蛮多的,不但可以分组,也可以分区,这里简单介绍几个,感兴趣小伙伴可以去看看 API 文档

getter分组

//getter分组 List<String> lists  = Arrays.asList("123", "123", "456", "789");lists.stream().collect(Collectors.groupingBy(String::hashCode))              .forEach((o1,o2) -> System.out.printf("%s:%s\n",o1,o2));==========48690:[123, 123]51669:[456]54648:[789]
复制代码

自定义逻辑分组

//2.自定义逻辑分组List<String> lists  = Arrays.asList("123", "1234", "4564", "789");lists.stream().collect(Collectors.groupingBy( o -> o.length()))              .forEach((o1,o2) -> System.out.printf("%s:%s\n",o1,o2));=========3:[123, 789]4:[1234, 4564]
复制代码

多级分组展示

// 多级分组List<String> list_  = Arrays.asList("123", "1234", "4564", "1234");list_.stream().collect(        Collectors.groupingBy(o -> o.length()                , Collectors.groupingBy(o1 -> o1.hashCode())))        .forEach((o1,o2) ->{            System.out.printf("--length:%s\n",o1);            o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:%s\n",o3,o4));        });========--length:3 |-hashCode:48690:[123]--length:4 |-hashCode:1509442:[1234, 1234] |-hashCode:1601791:[4564]        
复制代码

分组统计

List<String> list_  = Arrays.asList("123", "1234", "4564", "1234");list_.stream().collect(        Collectors.groupingBy(o -> o.length()                , Collectors.groupingBy(o1 -> o1.hashCode()                        , Collectors.counting())))        .forEach((o1,o2) ->{            System.out.printf("--length:%s\n",o1);            o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:sum:%s\n",o3,o4));        });==========--length:3 |-hashCode:48690:sum:1--length:4 |-hashCode:1509442:sum:2 |-hashCode:1601791:sum:1
复制代码

把收集器的结果转换为另一种类型

// 把收集器的结果转换为另一种类型,按照长度排序得到最大值,然后给Optional修饰List<String> list_  = Arrays.asList("123", "1234", "4564", "1234");list_.stream().collect(        Collectors.groupingBy(o -> o.length()                , Collectors.groupingBy(o1 -> o1.hashCode()                        , Collectors.collectingAndThen(                                Collectors.maxBy(                                        Comparator.comparingInt(String::length))                , Optional::get))))        .forEach((o1,o2) ->{    System.out.printf("--length:%s\n",o1);    o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:max:%s\n",o3,o4));});=========--length:3 |-hashCode:48690:max:123--length:4 |-hashCode:1509442:max:1234 |-hashCode:1601791:max:4564
复制代码

JS

JavaScript 新增了数组实例方法group()和groupToMap(),可以根据分组函数的运行结果,将数组成员分组。目前还是一个提案,需要考虑浏览器兼容,按照字符串分组就使用group(),按照对象分组就使用groupToMap()。所以groupToMap()和 Java 的分组很类似。


Experimental: This is an experimental technologyCheck the Browser compatibility table carefully before using this in production.
复制代码

group(function(element, index, array) {}, thisArg)

const array = [1, 2, 3, 4, 5];
array.group((num, index, array) => {  return num % 2 === 0 ? 'even': 'odd';});// { odd: [1, 3, 5], even: [2, 4] }
复制代码

groupToMap(function(element, index, array) { }, thisArg)

groupToMap()的作用和用法与 group()完全一致,唯一的区别是返回值是一个Map 结构,而不是对象

const array = [1, 2, 3, 4, 5];
const odd  = { odd: true };const even = { even: true };array.groupToMap((num, index, array) => {  return num % 2 === 0 ? even: odd;});//  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
复制代码

如果分组函数是一个箭头函数,thisArg对象无效,因为箭头函数内部的this是固化的,类似于 Ajax 回调内部的 this。

forEach 消费

forEach 这个不多讲,用于消费

java

List<String> list_  = Arrays.asList("123", "1234", "4564", "1234");list_.forEach(System.out::print);==============123123445641234
复制代码

JS

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },             { name: "毋固", value: "202203" }, { name: "毋我", value: "202204" }]
users.forEach(o => console.log(o))==========={ name: '毋意', value: '202201' }{ name: '毋必', value: '202202' }{ name: '毋固', value: '202203' }{ name: '毋我', value: '202204' }
复制代码

count 统计

count 也不多讲

java

List<String> lists_ = Arrays.asList("123", "1234", "4564", "1234");// 统计数据量System.out.println(lists_.stream().collect(Collectors.counting()));// 简单写法:System.out.println(lists_.stream().count());=========44
复制代码

JS

在 JS 中没有对应的方法,不过 Set 和 Map 有对应的 API,Array 的可以使用Array.prototype.length

reduce 归约

把数据源中的元素反复结合起来,得到一个值,即将流归约为一个值,用函数式编程语言叫折叠

java

Java 中的归约分为两种,一种为有初值的归约,一种为没有初值的归约。有初值的返回初值类型,没初值的返回一个 Options

T reduce(T identity, BinaryOperator<T> accumulator);

List<Integer> numbers1 = Arrays.asList(1, 2, 34, 5, 6);// 元素求和int set = numbers1.stream().reduce(0,(a,b) -> a + b);// 改进set = numbers1.stream().reduce(0, Integer::sum);
复制代码

Optional<T> reduce(BinaryOperator<T> accumulator)

List<Integer> numbers1 = Arrays.asList(1, 2, 34, 5, 6);//元素求最大值int set = numbers1.stream().reduce(Integer::max).get();// 元素求最小值set = numbers1.stream().reduce(Integer::min).get();
List<String> lists_ = Arrays.asList("123", "1234", "4564", "1234");System.out.println(lists_.stream().reduce((o1, o2) -> o1 + ',' + o2).get());==========123,1234,4564,1234
复制代码

JS

reduce((previousValue, currentValue, currentIndex, array) => {},initialValue)

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },             { name: "毋固", value: "202203" }, { name: "毋我", value: "202204" }]
let zy = users.map(o => o.name).reduce( (o1,o2) => o1+','+o2)console.log("子曰:子绝四,",zy)======子曰:子绝四, 毋意,毋必,毋固,毋我
复制代码

every/some|anyMatch/allMatch/noneMatch 谓词

所谓 谓词,即是否有满足条件的存在,返回一个布尔值。和 filter 特别像,只不过一个是中间操作,一个终端操作。

java

Java 中检查谓词是否至少匹配一个元素 ,使用anyMatch方法,即流中是否有一个元素能匹配给定谓词。boolean anyMatch(Predicate<? super T> predicate);

使用allMatch方法,即流中都能匹配所有元素返回ture, boolean allMatch(Predicate<? super T> predicate);

使用noneMatch方法,即流中都不能匹配所有元素返回true, boolean noneMatch(Predicate<? super T> predicate);

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);System.out.println(numbers.stream().anyMatch(o -> o > 5)); //trueSystem.out.println(numbers.stream().allMatch(o -> o > 0)); //trueSystem.out.println(numbers.stream().noneMatch(o -> o < 0)); //true
复制代码

JS

every()方法测试数组中的所有元素是否通过提供的函数实现的测试, every((element, index, array) => { /* ... */ } )

some()方法测试数组中的至少一个元素是否通过了提供的函数实现的测试, some((element, index, array) => { /* ... */ } )

let boo = Array.of(1, 2, 3, 4, 5, 6).every(o => o >5)console.log(boo) //falseboo = Array.of(1, 2, 3, 4, 5, 6).some(o => o >5)console.log(boo) //true
复制代码

findLast(findLastIndex)/find(findIndex)|findAny/findFirst 查找

查找元素:返回当前流的任意元素。

java

  • findAny()方法返回当前流的任意元素

  • findFirst()方法返回当前流的第一个元素。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);System.out.println(numbers.stream().findAny().get()); //1System.out.println(numbers.stream().findFirst().get()); //1
复制代码

JS

  • find()方法返回提供的数组中满足提供的测试功能的第一个元素

  • findIndex()方法返回满足提供的测试功能的数组中第一个元素的索引

let users = [{ name: "毋意", value: "202201" }, { name: "毋必", value: "202202" },{ name: "毋固", value: "202203" }, { name: "毋我", value: "202204" }]
let user = users.find(o => o.name === "毋固")console.log(user) //{ name: '毋固', value: '202203' }let useri = users.findIndex(o => o.name === "毋固")console.log(useri) //2
复制代码

这两个为 ES2022 新增,当前 Node 版本不支持

当前 Node 版本不支持

  • findLast()方法返回满足提供的测试功能的数组中最后一个元素的值

  • findLastIndex()方法返回满足提供的测试功能的数组中最后一个元素的索引

user = users.findLast(o => o.name === "毋固")console.log(user) useri = users.findLastIndex(o => o.name === "毋固")console.log(useri) 
复制代码

嗯,时间关系,关于对比就分享到这啦,其实还有好多,比如Stream API收集器等,还有好多奇技淫巧,感兴趣小伙伴可以看看下的书籍和网站

博文参考

发布于: 19 小时前阅读数: 33
用户头像

CSDN博客专家,华为云云享专家,RHCE/CKA认证 2022.01.04 加入

Java 后端一枚,技术不高,前端、Shell、Python 也可以写一点.纯种屌丝,不热爱生活,热爱学习,热爱工作,喜欢一直忙,不闲着。喜欢篆刻,喜欢吃好吃的,喜欢吃饱了晒太阳。

评论

发布
暂无评论
关于Java&JavaScript中(伪)Stream式API对比的一些笔记_Java_山河已无恙_InfoQ写作社区