写在前面


  • 前些时日开发遇到,想着把这些对比总结下
  • 博文内容包括:
  • 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:%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的分组很类似。

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

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

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

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

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