【Java 8系列】Stream详解,看这一篇就够啦_java8 stream-程序员宅基地

技术标签: Java专栏  java stream  java8 流管道  stream  java8 stream  java8新特性  

热门系列:


目录

1.前言

1.1 为什么要用Stream

1.2 什么是聚合操作

2.正文

2.1 Stream操作分类

2.2 Stream API使用

2.2.1 Stream 构成与创建

2.2.2 无状态(Stateless)操作

2.2.3 有状态(Stateful)操作

2.2.4 短路(Short-circuiting)操作

2.2.5 非短路(Unshort-circuiting)操作

3.总结


1.前言

Java 8的另一大亮点Stream,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。

Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。

1.1 为什么要用Stream

我个人总结有如下几个特点:

  • 有高效率的并行操作
  • 有多中功能性的聚合操作
  • 函数式编程,使代码更加简洁,提高编程效率

1.2 什么是聚合操作

举个例子,例如我们现在有一个模块的列表需要做如下处理:

  • 客户每月平均消费金额
  • 最昂贵的在售商品
  • 本周完成的有效订单(排除了无效的)
  • 取十个数据样本作为首页推荐

以上这些操作,你可以理解为就是对一个列表集合的聚合操作啦,类似于SQL里面的(count()、sum()、avg()....)!

有一些操作,有人可能会说,可以在SQL语句中完成过滤分类!首先不说SQL不能实现的功能,即使SQL能够实现,但是数据库毕竟是用来读写数据的,主要功能是用于数据落地存储的。并不是用来做大量的逻辑处理的,所以不能为了图方便,而忽略了性能方面的损耗!所以,相比之下,有一些列表操作我们必须在程序中做逻辑处理!那如果我们用之前的java处理方式,得像如下操作一样:

for(int i=0;i<10;i++){
    if(....){
        //内部做一系列的逻辑判断处理
        //也
        //许
        //有
        //这
        //么
        //多
        //行
        //还
        //不
        //止
    }else{
        //吧啦吧啦吧啦.......
    }

}

那如果用Stream来处理的话,可能就只有如下简单几行:

list.stream().filter().limit(10).foreach();

所以,代码不仅简洁了,阅读起来也会很是方便!


2.正文

2.1 Stream操作分类

Stream的操作可以分为两大类:中间操作、终结操作

中间操作可分为:

  • 无状态(Stateless)操作:指元素的处理不受之前元素的影响
  • 有状态(Stateful)操作:指该操作只有拿到所有元素之后才能继续下去

终结操作可分为:

  • 短路(Short-circuiting)操作:指遇到某些符合条件的元素就可以得到最终结果
  • 非短路(Unshort-circuiting)操作:指必须处理完所有元素才能得到最终结果

Stream结合具体操作,大致可分为如下图所示:

2.2 Stream API使用

接下来,我们将按各种类型的操作,对一些常用的功能API进行一一讲解:

2.2.1 Stream 构成与创建

2.2.1.1 流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

如下图所示:

图 1. 流管道 (Stream Pipeline) 的构成

2.2.1.2 流的创建

  • 通过 java.util.Collection.stream() 方法用集合创建流
List<String> list = Arrays.asList("hello","world","stream");
//创建顺序流
Stream<String> stream = list.stream();
//创建并行流
Stream<String> parallelStream = list.parallelStream();
  • 使用java.util.Arrays.stream(T[] array)方法用数组创建流
String[] array = {"h", "e", "l", "l", "o"};
Stream<String> arrayStream = Arrays.stream(array);
  • Stream的静态方法:of()、iterate()、generate()
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(3);
stream2.forEach(System.out::println);

Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println)


//输出结果如下:

0
2
4
0.9620319103852426
0.8303672905658537
0.09203215202737569
  • streamparallelStream的简单区分

stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,需要注意使用并行流的前提是流中的数据处理没有顺序要求(会乱序,即使用了forEachOrdered。例如筛选集合中的奇数,两者的处理不同之处:

image-20201125155309486

当然,除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流:

Optional<Integer> findFirst = list.stream().parallel().filter(x->x>4).findFirst();

2.2.2 无状态(Stateless)操作

  •  filter筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。
Stream<T> filter(Predicate<? super T> predicate);

流程解析图如下:

20201109144706541

举个栗子:

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2);
    Stream<Integer> stream = list.stream();
    stream.filter(x -> x > 5).forEach(System.out::println);
}



//结果如下:

6
7
8

  • 映射(map、flatMap、peek)

①map:一个元素类型为 T 的流转换成元素类型为 R 的流,这个方法传入一个Function的函数式接口,接收一个泛型T,返回泛型R,map函数的定义,返回的流,表示的泛型是R对象;

简言之:将集合中的元素A转换成想要得到的B

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

流程解析图如下:

Snipaste_2020-11-27_11-44-32

举个栗子:

//使用的People对象
public class People {
    private String name;
    private int age;
    ...省略get,set方法
}

//将String转化为People对象
Stream.of("小王:18","小杨:20").map(new Function<String, People>() {
     @Override
     public People apply(String s) {
         String[] str = s.split(":");
         People people = new People(str[0],Integer.valueOf(str[1]));
         return people;
     }
 }).forEach(people-> System.out.println("people = " + people));
}

或如下(众多姿势,任君选择):

List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());

②flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

简言之:与Map功能类似,区别在于将结合A的流转换成B流

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

流程解析图如下:

2020110914550762

举个栗子:

public static void main(String[] args) {
    List<String> list1 = Arrays.asList("m,k,l,a", "1,3,5,7");
    List<String> listNew = list1.stream().flatMap(s -> {
        // 将每个元素转换成一个stream
        String[] split = s.split(",");
        Stream<String> s2 = Arrays.stream(split);
        return s2;
    }).collect(Collectors.toList());

    System.out.println("处理前的集合:" + list1);
    System.out.println("处理后的集合:" + listNew);
}

//结果如下:
//这个结果的引号是不存在的,为了方便阅读,我手动添加的
处理前的集合:["m,k,l,a", "1,3,5,7"]
处理后的集合:["m", "k", "l", "a", "1", "3", "5", "7"]

③peek:peek 操作接收的是一个 Consumer<T> 函数。顾名思义 peek 操作会按照 Consumer<T> 函数提供的逻辑去消费流中的每一个元素,同时有可能改变元素内部的一些属性。

Stream<T> peek(Consumer<? super T> action);

这里我们要提一下这个 Consumer<T> ,以理解什么是消费。

Consumer<T> 是一个函数接口。一个抽象方法 void accept(T t) 意为接受一个 T 类型的参数并将其消费掉。其实消费给我的感觉就是 “用掉” ,自然返回的就是 void 。通常“用掉” T 的方式为两种:

  • T 本身的 void 方法 比较典型的就是 setter 。

  • 把 T 交给其它接口(类)的 void 方法进行处理 比如我们经常用的打印一个对象 System.out.println(T)

操作流程解析图如下:

下面我们来看个栗子:

Stream<String> stream = Stream.of("hello", "felord.cn");
stream.peek(System.out::println);

执行之后,控制台并没有输出任何字符串!纳尼??

这是因为流的生命周期有三个阶段:

  • 起始生成阶段。

  • 中间操作会逐一获取元素并进行处理。可有可无。所有中间操作都是惰性的,因此,流在管道中流动之前,任何操作都不会产生任何影响。

  • 终端操作。通常分为 最终的消费 (foreach 之类的)和 归纳 (collect)两类。还有重要的一点就是终端操作启动了流在管道中的流动。

所以,上面的代码是因为缺少了终端操作,因此,我们改成如下即可:

Stream<String> stream = Stream.of("hello", "felord.cn");
stream.peek(System.out::println).collect(Collectors.toList());

//控制台打印内容如下:
hello
felord.cn

重点:peek VS map

他们最大的区别是:

peek 操作 一般用于不想改变流中元素本身的类型或者只想元素的内部状态时;

而 map 则用于改变流中元素本身类型,即从元素中派生出另一种类型的操作。

④mapToInt、mapToLong、mapToDouble、flatMapToDouble、flatMapToInt、flatMapToLong

以上这些操作是map和flatMap的特例版,也就是针对特定的数据类型进行映射处理。其对应的方法接口如下:

IntStream mapToInt(ToIntFunction<? super T> mapper);

LongStream mapToLong(ToLongFunction<? super T> mapper); 

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);

LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);

DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

此处就不全部单独说明了,取一个操作举例说明一下其用法:

Stream<String> stream = Stream.of("hello", "felord.cn");
stream.mapToInt(s->s.length()).forEach(System.out::println);


//输出结果
5
9

并且这些指定类型的流,还有另外一些常用的方法,也是很好用的,可以参考:IntStreamLongStreamDoubleStream

  • 无序化(unordered)

unordered()操作不会执行任何操作来显式地对流进行排序。它的作用是消除了流必须保持有序的约束,从而允许后续操作使用不必考虑排序的优化。

举个栗子:

public static void main(String[] args) {
	 Stream.of(5, 1, 2, 6, 3, 7, 4).unordered().forEach(System.out::println);
     Stream.of(5, 1, 2, 6, 3, 7,4).unordered().parallel().forEach(System.out::println);
}


//两次输出结果对比(方便比较,写在一起)
第一遍:          第二遍:
//第一行代码输出   //第一行代码输出
5                 5
1                 1
2                 2
6                 6
3                 3
7                 7
4                 4

//第二行代码输出   //第二行代码输出
3                 3
6                 6
4                 7
7                 5
2                 4
1                 1
5                 2

以上结果,可以看到,虽然用了unordered(),但是第一个循环里的数据顺序并没有被打乱;是不是很好奇?

您可以在Java 8文档中有一下一段内容:

对于顺序流,顺序的存在与否不会影响性能,只影响确定性。如果流是顺序的,则在相同的源上重复执行相同的流管道将产生相同的结果;

如果是非顺序流,重复执行可能会产生不同的结果。 对于并行流,放宽排序约束有时可以实现更高效的执行

在流有序时, 但用户不特别关心该顺序的情况下,使用 unordered 明确地对流进行去除有序约束可以改善某些有状态或终端操作的并行性能。

2.2.3 有状态(Stateful)操作

  • distinct:返回由该流的不同元素组成的流(根据 Object.equals(Object));distinct()使用hashCode()和equals()方法来获取不同的元素。因此,我们的类必须实现hashCode()和equals()方法。
Stream<T> distinct();

简言之:就是去重;下面看下流程解析图:

20201109150012790

举个栗子:

Stream<String> stream = Stream.of("1", "3","4","10","4","6","23","3");
stream.distinct().forEach(System.out::println);


//输出
1
3
4
10
6
23

可以发现,重复的数字会被剔除掉!那么如果需要对自定义的对象进行过滤,则需要重写对象的equals方法即可 !

另外有一个细节可以看到,去重之后还是按照原流中的排序顺序输出的,所以是有序的!

  • sorted:返回由该流的元素组成的流,并根据自然顺序排序

该接口有两种形式:无参和有参数,如:

Stream<T> sorted();

Stream<T> sorted(Comparator<? super T> comparator);

那区别其实就在于:传入比较器的参数,可以自定义这个比较器,即自定义比较规则

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
stream.sorted().forEach(System.out::println);	


//输出
1
3
4
8
9
10
16

  • limit:获取流中n个元素返回的流

这个很好理解,和mysql的中的limit函数一样的效果,返回指定个数的元素流。

Stream<T> limit(long maxSize);

流程解析图如下:

20201109145946301

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
stream.limit(3).forEach(System.out::println);


//输出
3
1
10

  • skip:在丢弃流的第一个n元素之后,返回由该流的其余元素组成的流。

简言之:跳过第n个元素,返回其后面的元素流;

Stream<T> skip(long n);

流程解析图:

20201109150001270

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
stream.skip(3).forEach(System.out::println);


//输出
16
8
4
9

2.2.4 短路(Short-circuiting)操作

  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true;
boolean anyMatch(Predicate<? super T> predicate);

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
System.out.println("result="+stream.anyMatch(s->s==2));

//输出
result=false

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true;
boolean allMatch(Predicate<? super T> predicate);

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
System.out.println("result="+stream.allMatch(s->s>=1));


//输出
result=true

  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true.
boolean noneMatch(Predicate<? super T> predicate);

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
System.out.println("result="+stream.noneMatch(s -> s>=17 ));


//输出
result=true

  • findFirst:用于返回满足条件的第一个元素(但是该元素是封装在Optional类中)

关于Optional可以点这里【Java 8系列】Java开发者的判空利器 -- Optional

Optional<T> findFirst();

举个栗子:

Stream<Integer> stream = Stream.of(3,1,10,16,8,4,9);
System.out.println("result="+stream.findFirst().get());

//输出
result=3


//当然,我们还可以结合filter处理
System.out.println("result="+stream.filter(s-> s > 3).findFirst().get());

//输出
result=10

  • findAny:返回流中的任意元素(但是该元素也是封装在Optional类中)
Optional<T> findAny();

举个栗子:

List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya");

String result = strAry.parallelStream().filter(s->s.startsWith("J")).findAny().get();
System.out.println("result = " + result);

//输出
result = Jill

通过多次执行,我们会发现,其实findAny会每次按顺序返回第一个元素。那这个时候,可能会认为findAny与findFirst方法是一样的效果。其实不然,findAny()操作,返回的元素是不确定的,对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个。

我们接着看下面这个例子:

List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya");
 
String result = strAry.parallelStream().filter(s->s.startsWith("J")).findAny().get();
System.out.println("result = " + result);

//输出
result = Jill
或
result = Julia

如此可见,在并行流里,findAny可就不是只返回第一个元素啦!

2.2.5 非短路(Unshort-circuiting)操作

  • forEach:该方法接收一个Lambda表达式,然后在Stream的每一个元素上执行该表达式

可以理解为我们平时使用的for循环,但是较于for循环,又略有不同!咱们待会再讲。

void forEach(Consumer<? super T> action);

举个栗子:

List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya");

strAry.stream().forEach(s-> {
			if("Jack".equalsIgnoreCase(s)) System.out.println(s);
		});

//输出
Jack

那如果我们把 "Jack"用在循环外部用一个变量接收,如下操作:

String name = "Jack";
strAry.stream().forEach(s-> {
	if(name.equalsIgnoreCase(s)) name = "Jackson";
});

那么此时编辑器则会爆红,

因为lambda中,使用的外部变量必须是最终的,不可变的,所以如果我们想要对其进行修改,那是不可能的!如果必须这么使用,可以将外部变量,移至表达式之中使用才行!

  • forEachOrdered:该方法接收一个Lambda表达式,然后按顺序在Stream的每一个元素上执行该表达式
void forEachOrdered(Consumer<? super T> action);

该功能其实和forEach是很相似的,也是循环操作!那唯一的区别,就在于forEachOrdered是可以保证循环时元素是按原来的顺序逐个循环的!

但是,也不尽其然!因为有的时候,forEachOrdered也是不能百分百保证有序!例如下面这个例子:

Stream.of("AAA,","BBB,","CCC,","DDD,").parallel().forEach(System.out::print);
System.out.println("\n______________________________________________");
Stream.of("AAA,","BBB,","CCC,","DDD").parallel().forEachOrdered(System.out::print);
System.out.println("\n______________________________________________");
Stream.of("DDD,","AAA,","BBB,","CCC").parallel().forEachOrdered(System.out::print);

//输出为:
 
CCC,DDD,BBB,AAA,
______________________________________________
AAA,BBB,CCC,DDD
______________________________________________
DDD,AAA,BBB,CCC

可以看到,在并行流时,由于是多线程处理,其实还是无法保证有序操作的!

  • toArray:返回包含此流元素的数组;当有参数时,则使用提供的generator函数分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组
Object [] toArray();

<A> A[] toArray(IntFunction<A[]> generator);

举个栗子:

List<String> strList = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya");

Object [] strAryNoArg = strList.stream().toArray();
String [] strAry = strList.stream().toArray(String[]::new);

  • reduce:方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值

通过字面意思,可能比较难理解是个什么意思?下面我们先看一个图,熟悉一下这个接口的操作流程是怎样的:

该接口含有3种调用方式:

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

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

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);


//以及参数的定义结构
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
	//两个静态方法,先进行忽略
}

@FunctionalInterface
public interface BiFunction<T, U, R> {
	R apply(T t, U u);
	//一个默认方法,先进行忽略
}

下面举几个栗子,看看具体效果:

(一).先以1个参数的接口为例

为了方便理解,先看下内部的执行效果代码:

boolean foundAny = false;
T result = null;
for (T element : this stream) {
    if (!foundAny) {
        foundAny = true;
        result = element;
    }
    else
        result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

再看下具体栗子:

List<Integer> num = Arrays.asList(1, 2, 4, 5, 6, 7);

*原接口一比一原汁原味写法*
Integer integer = num.stream().reduce(new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer a, Integer b) {
    	System.out.println("x:"+a);
        return a + b;
    }
}).get();
System.out.println("resutl:"+integer);


*等效写法一*
Integer result = num.stream().reduce((x, y) -> {
    System.out.println("x:"+x);
    return x + y;
}).get();
System.out.println("resutl:"+result);


*等效的普通写法*
boolean flag = false;
int temp = 0;
for (Integer integer : num) {
	if(!flag){
		temp = integer;
		flag = true;
	}else {
		System.out.println("x:"+temp);
		temp += integer;
	}
}

System.out.println("resutl:"+temp);

执行结果都是:

x:1
x:3
x:7
x:12
x:18
resutl:25

(二)再以2个参数的接口为例

先看下内部的执行效果代码:

T result = identity;
for (T element : this stream){
    result = accumulator.apply(result, element)
}
return result;

在看具体栗子:

List<Integer> num = Arrays.asList(1, 2, 4, 5, 6, 7);

*一比一原汁原味写法*
Integer integer = num.stream().reduce(1,new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer a, Integer b) {
    	System.out.println("a="+a);
        return a + b;
    }
});
System.out.println("resutl:"+integer);


*普通for循环写法*
int temp = 1;
for (Integer integer : num) {
	System.out.println("a="+temp);
	temp += integer;
}

System.out.println("resutl:"+temp);

输出结果都是:

a=1
a=2
a=4
a=8
a=13
a=19
resutl:26

(三)最后3个参数的接口为例

这个接口的内部执行效果,其实和2个参数的几乎一致。那么第三个参数是啥呢?这是一个combiner组合器;

组合器需要和累加器的返回类型需要进行兼容,combiner组合器的方法主要用在并行操作中

在看具体栗子:

List<Integer> num = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> other = new ArrayList<>();
other.addAll(Arrays.asList(7,8,9,10));

num.stream().reduce(other,
	(x, y) -> { //第二个参数
		System.out.println(JSON.toJSONString(x));
        x.add(y);
        return x;
    },
	(x, y) -> { //第三个参数
        System.out.println("并行才会出现:"+JSON.toJSONString(x));
        return x;
});




//输出结果:
[7,8,9,10,1]
[7,8,9,10,1,2]
[7,8,9,10,1,2,3]
[7,8,9,10,1,2,3,4]
[7,8,9,10,1,2,3,4,5]
[7,8,9,10,1,2,3,4,5,6]

我们再讲串行流改成并行流,看下会出现什么结果:

List<Integer> num = Arrays.asList( 4, 5, 6);
List<Integer> other = new ArrayList<>();
other.addAll(Arrays.asList(1,2,3));

num.parallelStream().reduce(other,
	(x, y) -> { //第二个参数
        x.add(y);
        System.out.println(JSON.toJSONString(x));
        return x;
    },
	(x, y) -> { //第三个参数
		x.addAll(y);
		System.out.println("结合:"+JSON.toJSONString(x));
		return x;
});


//输出结果
//第一遍
[1,2,3,4,5,6]
[1,2,3,4,5,6]
[1,2,3,4,5,6]
结合:[1,2,3,4,5,6,1,2,3,4,5,6]
结合:[1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6,1,2,3,4,5,6]

//第二遍
[1,2,3,4,6]
[1,2,3,4,6]
[1,2,3,4,6]
结合:[1,2,3,4,6,1,2,3,4,6]
结合:[1,2,3,4,6,1,2,3,4,6,1,2,3,4,6,1,2,3,4,6]

//第三遍
[1,2,3,5,4,6]
[1,2,3,5,4,6]
[1,2,3,5,4,6]
结合:[1,2,3,5,4,6,1,2,3,5,4,6]
结合:[1,2,3,5,4,6,1,2,3,5,4,6,1,2,3,5,4,6,1,2,3,5,4,6]

我们会发现每个结果都是乱序的,并且多执行几次,都会出现不同的结果。并且第三个参数组合器内的代码也得到了执行!!

这就是因为并行时,使用多线程时顺序性没有保障所产生的结果。通过实践,可以看到:组合器的作用,其实是对参数2中的各个线程,产生的结果进行了再一遍的归约操作!

并且仔细看第二遍的执行结果:每一组都少了一1个值!!!

所以,对于并行流parallelStream操作,必须慎用!!

  • collect:称为收集器,是一个终端操作,它接收的参数是将流中的元素累积到汇总结果的各种方式
<R, A> R collect(Collector<? super T, A, R> collector);

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

第一种方式会比较经常使用到,也比较方便使用,现在先看一看里面常用的一些方法:

工厂方法

返回类型

用于

toList

List<T>

把流中所有元素收集到List中

示例:List<Menu> menus=Menu.getMenus.stream().collect(Collectors.toList());

toSet

Set<T>

把流中所有元素收集到Set中,删除重复项

示例:Set<Menu> menus=Menu.getMenus.stream().collect(Collectors.toSet());

toCollection

Collection<T>

把流中所有元素收集到给定的供应源创建的集合中

示例:ArrayList<Menu> menus=Menu.getMenus.stream().collect(Collectors.toCollection(ArrayList::new));

Counting

Long

计算流中元素个数

示例:Long count=Menu.getMenus.stream().collect(counting);

SummingInt

Integer

对流中元素的一个整数属性求和

示例:Integer count=Menu.getMenus.stream().collect(summingInt(Menu::getCalories));

averagingInt

Double

计算流中元素integer属性的平均值

示例:Double averaging=Menu.getMenus.stream().collect(averagingInt(Menu::getCalories));

Joining

String

连接流中每个元素的toString方法生成的字符串

示例:String name=Menu.getMenus.stream().map(Menu::getName).collect(joining(“, ”));

maxBy

Optional<T>

一个包裹了流中按照给定比较器选出的最大元素的optional
如果为空返回的是Optional.empty()

示例:Optional<Menu> fattest=Menu.getMenus.stream().collect(maxBy(Menu::getCalories));

minBy

Optional<T>

一个包裹了流中按照给定比较器选出的最小元素的optional
如果为空返回的是Optional.empty()

示例: Optional<Menu> lessest=Menu.getMenus.stream().collect(minBy(Menu::getCalories));

Reducing

归约操作产生的类型

从一个作为累加器的初始值开始,利用binaryOperator与流中的元素逐个结合,从而将流归约为单个值

示例:int count=Menu.getMenus.stream().collect(reducing(0,Menu::getCalories,Integer::sum));

collectingAndThen

转换函数返回的类型

包裹另一个转换器,对其结果应用转换函数

示例:Int count=Menu.getMenus.stream().collect(collectingAndThen(toList(),List::size));

groupingBy

Map<K,List<T>>

根据流中元素的某个值对流中的元素进行分组,并将属性值做为结果map的键

示例:Map<Type,List<Menu>> menuType=Menu.getMenus.stream().collect(groupingby(Menu::getType));

partitioningBy

Map<Boolean,List<T>>

根据流中每个元素应用谓语的结果来对项目进行分区

示例:Map<Boolean,List<Menu>> menuType=Menu.getMenus.stream().collect(partitioningBy(Menu::isType)

第二种方式看起来跟reduce的三个入参的方法有点类似,也可以用来实现filter、map等操作!

流程解析图如下:

avatar

举个栗子:

List<Integer> numList = Arrays.asList(1,2,3);
		
numList.stream()
        .collect(()->{ //第一个参数
        	//构造器
            System.out.println("构造器,返回一个你想用到的任意起始对象!此处返回一个空List为例");
            System.out.println();
            return new ArrayList();
        }, (a, b) -> {  //第二个参数
            synchronized (Java8DemoServiceImpl.class) {  //加锁是为了并行流下方便查看打印结果
                System.out.println("累加器");
                a.forEach(item -> System.out.println("a:" + item));
                System.out.println("b:" + b);
                
                a.add(b);
                //换行
                System.out.println();
            }
        }, (a, b) -> {  //第三个参数
            synchronized (Java8DemoServiceImpl.class) {  //加锁是为了并行流下方便查看打印结果
                System.out.println("合并器");
                System.out.println("a:" + JSON.toJSONString(a) + " , " + "b:" + JSON.toJSONString(b));
                
                a.addAll(b);
                System.out.println();  //为了换行方便查看打印
            }
        })
        .forEach(item -> System.out.println("最终结果项:" + item));

运行结果:

构造器,返回一个你想用到的任意起始对象!此处返回一个空List为例

累加器
b:1

累加器
a:1
b:2

累加器
a:1
a:2
b:3

最终结果项:1
最终结果项:2
最终结果项:3

如果把上述流换成并行流,会得到如下一种结果:

构造器,返回一个你想用到的任意起始对象!此处返回一个空List为例
构造器,返回一个你想用到的任意起始对象!此处返回一个空List为例

构造器,返回一个你想用到的任意起始对象!此处返回一个空List为例

累加器

b:2

累加器
b:1

累加器
b:3

合并器
a:[2] , b:[3]

合并器
a:[1] , b:[2,3]

最终结果项:1
最终结果项:2
最终结果项:3

可以看到,根据流内的元素个数n,起了n个线程,同时分别执行了构造器、累加器、合并器内代码!与reduce的行为方式基本一致!

  • max:根据提供的Comparator返回此流的最大元素
Optional<T> max(Comparator<? super T> comparator);

举个栗子:

List<Integer> num = Arrays.asList( 4, 5, 6);
num.stream().max(Integer::compareTo).ifPresent(System.out::println);

//输出
6

  • min:根据提供的Comparator返回此流的最小元素
Optional<T> min(Comparator<? super T> comparator);

举个栗子:

List<Integer> num = Arrays.asList( 4, 5, 6);
num.stream().min(Integer::compareTo).ifPresent(System.out::println);

//输出
4

  • count:返回此流中的元素计数
long count();

举个栗子:

List<Integer> num = Arrays.asList( 4, 5, 6);
System.out.println(num.stream().count());

//输出
3


3.总结

此处给正在学习的朋友两个小提示:

1、对于流的各种操作所属分类,还不够熟悉的,可以直接进入方法的源码接口内,如下,是可以查看到类型说明的:

2、对于并行流stream().parallel()、parallelStream()的使用,须慎重使用!使用前须考虑其不确定因素和无序性,考虑多线程所带来的复杂性!!

2020年已近年末,再过几天就要步入新年啦!工作之余,耗时几天,终于写完了这篇博文!分享不易,希望感兴趣的朋友,可以留言讨论,点赞收藏!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yy339452689/article/details/110956119

智能推荐

教你看懂邮件头信息<转载>-程序员宅基地

文章浏览阅读1.2k次。MIME对于邮件系统的扩展是巨大的,因为在MIME出现以前,信件内容如果要包括声音和动画,就必须把它变为ASCII码或把二进制的信息变成可以传送的编码标准,而接收方必须经过解码才可以获得声音和图画信息。MIME提供了一种可以在邮件中附加多种不同编码文件的方法。一封邮件主要由邮件头和邮件体组成.邮件头包含了发件人、收件人、主题、时间、MIME版本、邮件内容的类型等重要信息。每条信息称为一个域,由..._邮件头信息包含哪些内容

GNN初学笔记(一)—— Relational inductive biases, deep learning, and graph networks简要解析-程序员宅基地

文章浏览阅读2.2k次,点赞3次,收藏20次。GNN初学笔记(一)—— Relational inductive biases, deep learning, and graph networks文献学习(我会在每个术语解释后面加入自己的理解,若有错误恳请指正)这篇文献首先阐释AI如何模拟人类的学习能力,即组合泛化能力(combinatorial generalization),总结了目前深度学习“端到端”(end to en..._relational inductive biases

springboot项目整合jsp_springboot+jsp的项目-程序员宅基地

文章浏览阅读527次。这篇文章的目的是在springboot项目中使用jsp的完整过程,包含项目的创建和打包。下面便开始吧。一、新建项目新建项目流程就是新建一个普通的springboot项目,下面贴几张创建项目过程中的图二、修改pom修改项目的打包方式为war <packaging>war</packaging>引入jsp的依赖和web的依赖<dependency>..._springboot+jsp的项目

signal(SIGCHLD,SIG_DFL)和signal(SIGCHLD,SIG_IGN)的区别 默认忽略和主动忽略 为什么父进程前面父进程会产生僵尸进程而后面会杀死僵尸进程_signal(sigchld, sig_dfl)-程序员宅基地

文章浏览阅读2.1k次。signal(SIGCHLD,SIG_DFL)就是默认的忽略子进程结束信号,默认忽略子进程结束信号的作用就是产生了僵尸进程后可以让你在孩子死后进行wait操作来结束僵尸进程。而主动进行signal(SIGCHLD,SIG_IGN)手动处理信号,操作系统会立即删除进程表中的子进程并且子进程死亡时,并不创建僵尸。由操作系统内核回收。..._signal(sigchld, sig_dfl)

【蓝桥】算法能力恢复性练习3(next_permutation()、vector())_next permutation和vector-程序员宅基地

文章浏览阅读185次。博客内容说明本系列博客:《算法能力恢复性练习》,主要用于记录个人备战2020年蓝桥杯,练习算法能力与技巧的过程。代码和思路不一定正确,部分代码和思路参考自他人,侵请删。欢迎大家交流学习,指正错误!推荐练习网址:蓝桥杯ACM训练习题1、简单暴力题标题:猜年龄小明带两个妹妹参加元宵灯会。别人问她们多大了,她们调皮地说:“我们俩的年龄之积是年龄之和的6倍”。小明又补充说:“她们可不是双胞胎..._next permutation和vector

计算机摄像头打不开,电脑摄像头打不开、用不了怎么办(操作简单),这几步你要了解...-程序员宅基地

文章浏览阅读7.7k次。有时候我们会遇到在开视频时电脑无法打开摄像头,这有可能是摄像头本身就有问题,也有可能是因为权限设置美没有到位,导致摄像头不能打开。今天咱们来解决一下,电脑摄像头无法打开的问题。工具/材料·鲁大师或者360驱动大师等可以修改驱动的软件·带有摄像头的电脑操作方法01首先咱们先找到原因,导致是驱动有问题还是摄像头设备的问题。所以我们可以在电脑左下角的输入框内输入“相机”(使用的是win10系统),来打开..._电脑相机摄像头打不开

随便推点

Cocos2d-x Js Binding 的手动绑定实现_protokeytoclass jsfriendapi.h-程序员宅基地

文章浏览阅读1.5k次。http://www.ityran.com/archives/4902Cocos2d-x Js Binding 的手动绑定实现一叶 cocos2d-x 08-13 2304 4条评论随着 Cocos2d-x 的发展,Cocos2d-html5 也日益完善,相比纯 C++ 的开发方式,它开发效率更为高效,而另一个显而易见的好处便是 JS 端的 API 可以作为_protokeytoclass jsfriendapi.h

CDOJ_1147 (最短路条数)_最短路 条数-程序员宅基地

文章浏览阅读1.9k次。分析:最短路条数问题。求最短路的条数只需要在dijkstra上面加一个数组sumt[]记录就行,sumt[v] 表示从源点 s 出发到 v 的最短路条数,当 dist[v] > dist[u] + d[u][v] 时,更新sumt[v] 的值就是 sumt[u];当 dist[v] == dist[u] + d[u][v] 时,sumt[v] += sumt[u];判断是否存在无数条最短路,即看是否存在这样的一条边(u, v),边权为 0,并且其中一条最短路经过这条边,也就是 源点 s 到 u 的最短距离_最短路 条数

Nginx+Center OS 7.2 开机启动设置_centeros开机命令-程序员宅基地

文章浏览阅读124次。centos 7以上是用Systemd进行系统初始化的,Systemd 是 Linux 系统中最新的初始化系统(init),它主要的设计目标是克服 sysvinit 固有的缺点,提高系统的启动速度。关于Systemd的详情介绍在这里。Systemd服务文件以.service结尾,比如现在要建立nginx为开机启动,如果用yum install命令安装的,yum命令会自动创建nginx.service文件,直接用命令systemcel enable nginx.service设置开机启动即可。_centeros开机命令

01天翎MyApps开发平台的安装_myapps5.1安装部署-程序员宅基地

文章浏览阅读1.1k次。一、安装以管理员身份权限运行安装安装目录:D:\MyApps二、登陆地址:普通用户前台登录:http://localhost:8080/obpm帐号/密码:admin1/123456后台企业域登录:http://localhost:8080/obpm/admin帐号/密码:admin/teemlink后台软件管理登录:http://lo..._myapps5.1安装部署

Golang 入门-程序员宅基地

文章浏览阅读2.4k次,点赞37次,收藏40次。Golang(又称为 Go)是 Google 公司开发出的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go 语言保证了既能够达到 静态编译语言的安全和性能,又达到了 动态开发语言维护的高效率,使用一个表达式来形容 Go 语言:Go = C + Python,说明 Go 语言既有 C 静态语言程序的运行速度,又能达到 Python 动态语言的快速开发。_golang

数组,二维数组,以及简单的冒泡排序实现_c++二维数组冒泡排序-程序员宅基地

文章浏览阅读166次。数组的定义 相同数据类型的有序集合 若干个相同数据类型的数据,按照一定的先后次序排序而成 每一个数据称为数组元素,每个数组元素可以通过下标来访问(PS:下标从0开始)数组的四个基本特点 数组长度是确定的,一旦被创建打小就是不可变的 其中元素必须是相同类型,不能是混合类型 数组中的元素可以是八大基本,也可以是引用类型 数组变量属于引用类型,数组也可以看成是对象,数组中的每个元素可以看成是该对象的成员变量。 数组本身就是对象,Java中对象存放_c++二维数组冒泡排序

推荐文章

热门文章

相关标签