您的位置 首页 java

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

写在前面

  • 前些时日开发遇到,想着把这些对比总结下
  • 博文内容包括: Stream 相关概念简述, Java 和JavaScript的Stream式API对比Demo
  • 食用方式 博文适合会一点前端的Java后端&会一点Java后端的前端,需要了解Java&JavaScript基础知识
  • 理解不足小伙伴帮忙指正

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


什么是流(Stream)

关于 Stream , 在Java中我们叫 ,但是在JavaScript中,好像没有这种叫,也没有 Stream API,我么姑且称为 伪流 ,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差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从 流中排序和删除重复项时都需要知道先前的历史 。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题。我们把这些操作叫作 有状态操作

中间操作

JavaScript Java 说明 filter filter 筛选 map map 映射 flatMap flatMap 扁平化 slice limit 截断 sort sorted 排序 不支持 distinct 去重 slice skip 跳过 group/groupToMap groupingBy 分组

终端操作

JavaScript Java 说明 forEach forEach 消费 length count 统计 reduce/reduceRight reduce 归约 every/some anyMatch/allMatch/noneMatch 谓词/短路求值 findLast(findLastIndex)/find(findIndex) findAny/findFirst 查找

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);

======
13
4
5
6
5

  

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);
====
H
e
l
l
o
W
o
r
l
d        
  

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 :H
out :e
out :l
out :l
out :o
out :W
out :o
out :r
out :l
out :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);
=====
12
3
  

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);
===========
3
4
4
5
12
  

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);
=========
12
3
4
5
  

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);
==================
4
5
4

  

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);
}
  
 @FunctionalInterface
public 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:%sn",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:%sn",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:%sn",o1);
            o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:%sn",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:%sn",o1);
            o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:sum:%sn",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:%sn",o1);
    o2.forEach((o3,o4) ->System.out.printf(" |-hashCode:%s:max:%sn",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 technology
Check 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());
=========
4
4
  

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)); 
//true
System.out.println(numbers.stream().allMatch(o -> o > 0)); 
//true
System.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) //false
boo = 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()); //1
System.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 收集器 等,还有好多 奇技淫巧 ,感兴趣小伙伴可以看看下的书籍和网站

博文参考

  • 《Java8 实战》
  • 《ECMAScript 6 入门教程》
  • #sec-array-objects

文章来源:智云一二三科技

文章标题:关于Java&JavaScript中(伪)Stream式API对比的一些笔记

文章地址:https://www.zhihuclub.com/200843.shtml

关于作者: 智云科技

热门文章

网站地图