4

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

 2 years ago
source link: https://liruilongs.github.io/2022/07/16/Java/%E5%85%B3%E4%BA%8E%20Java&JavaScript%E4%B8%ADStream%E5%BC%8FAPI%E5%AF%B9%E6%AF%94%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AC%94%E8%AE%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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


  • 前些时日开发遇到,想着把这些对比总结下
  • 博文内容包括:
    • 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差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题。我们把这些操作叫作有状态操作
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的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

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

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

<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

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"]

<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

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

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 的截断处理可以使用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 排序

排序,这个不多讲,

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

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方法实现)的流

Stream<T> distinct();

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

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)是互补的

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 中可以通过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 的分组通过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:%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

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 这个不多讲,用于消费

List<String> list_  = Arrays.asList("123", "1234", "4564", "1234");
list_.forEach(System.out::print);
==============
123123445641234
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 也不多讲

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中没有对应的方法,不过Set和Map有对应的API,Array的可以使用Array.prototype.length

reduce 归约

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

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

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中检查谓词是否至少匹配一个元素 ,使用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

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 查找

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

  • 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
  • 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版本不支持

在这里插入图片描述

在这里插入图片描述

  • findLast()方法返回满足提供的测试功能的数组中最后一个元素的值
  • findLastIndex()方法返回满足提供的测试功能的数组中最后一个元素的索引
    user = users.findLast(o => o.name === "毋固")
    console.log(user)
    useri = users.findLastIndex(o => o.name === "毋固")
    console.log(useri)

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK