java8Stream操作

前言

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation)(例如统计最大/最小值,统计总和等),或者大批量数据操作 (bulk data operation)。它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

Stream

1、Stream是什么?

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream有以下特点:

  • 不存储数据。 流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  • 函数式编程。 流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
  • 延迟操作。 流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
  • 可以解绑。 对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
  • 纯消费。 流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。
  • 使用了Fork/Join。 Stream使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出
  • Stream数据源本身是可以无限的

2、Stream的基本使用流程

当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

3、获取一个数据源(Stream Source)的方式

  • Collection 提供的stream()方法和parallelStream()方法
  • Arrays类 提供的静态stream()方法可以将数组转换为流
  • BufferedReader类 提供了lines()方法
  • 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier s)如Stream.generate(Math::random)
  • Files类的操作路径的方法,如list、find、walk等。
  • 随机数流Random.ints()。
  • 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。
  • 更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法。

4、流的操作

流有两种操作类型:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
    例子:
        Long[] nums = new Long[]{2L,6786L,362874L,22342L,1143241L};
        long sum = Arrays.stream(nums)
                .filter(n->n > 1000)
                .mapToInt(n->n.intValue())
                .sum();

上面的例子里,stream方法获取数据源,filter和mapToInt方法时intermediate操作,用来对流做过滤和数据映射,sum是terminal操作,用来计算总和。

5、流的详解

5.1、常用的流的构造方法

//方式一、使用Stream的静态方法of
Stream<String> stream1 = Stream.of("a", "b", "c");

//方式二、使用集合类的stream方法
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream2 = list.stream();

//方式三、使用Arrays类的静态方法
Stream<String> stream3 =Arrays.stream(new String[]{"a","b","v"});

注意:对于基本数值型,目前有三种对应的包装类型 Stream:

  • IntStream
  • LongStream
  • DoubleStream

使用方式如下(以LongStream为例):

        LongStream stream = LongStream.builder()
                .add(1L)
                .add(2L)
                .build();

        LongStream stream2 = LongStream.of(new long[]{1L,2L});

        LongStream stream3 = LongStream.range(1L,5L);

        LongStream stream4 = LongStream.rangeClosed(1L,100L);

使用这种方式,而不是Stream< Long> stream = Stream.of…的方式是因为就装箱和拆箱效率来说,直接使用包装类型Stream的效率更高些。

5.2、Stream转换成数据结构

  • toArray方法将一个流转换成数组
  • Collectors.toXXX方法转换成其他数据结构

例子:

// 1. Array
String[] strArray1 = stream1.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream2.collect(Collectors.toList());
List<String> list2 = stream3.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream4.collect(Collectors.toSet());
Stack stack1 = stream5.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream6.collect(Collectors.joining()).toString();

5.3、intermediate操作

中间操作会返回一个新的流,并且操作是延迟执行的(lazy),它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。

5.3.1、distinct

distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。

Stream<String> stream = Stream.of(new String[]{"a","b","c","b"});
List<String> list = stream.distinct().collect(Collectors.toList());
list.forEach(System.out::println);
5.3.2、filter

filter返回的流中只包含满足断言(predicate)的数据。

IntStream stream  = IntStream.of(new int[] {10,20,30,40,50});
List<Integer> res = stream.filter(n->n>20).boxed().collect(Collectors.toList());
res.forEach(System.out::println);
5.3.3、map

map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。

Stream<String> stream = Arrays.asList("10", "20", "30", "40").stream();
List<Integer> res = stream.mapToInt(str -> Integer.parseInt(str))
                .filter(n -> n > 20)
                .boxed()
                .collect(Collectors.toList());
res.forEach(System.out::println);
5.3.4、flatmap

Java8中的Stream流可以处理通过map方法处理基本类型及(String),但却无法对数组和列表进行操作,flatMap方法弥补了这一不足。flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起。看一个简单的例子:

        List<String> list1 = Arrays.asList("a", "b", "c");
        List<String> list2 = Arrays.asList("aa", "bb", "cc");
        List<String> list3 = Arrays.asList("aaa", "bbb", "ccc");

        Stream<List<String>> stream = Stream.of(list1,list2,list3);
        List res = stream.flatMap(p->p.stream()).collect(Collectors.toList());
        res.forEach(System.out::println);

结果是:
在这里插入图片描述

5.3.5、limit

limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。

        List<String> list = Arrays.asList("a", "b", "c","d");

        Stream<String> stream = list.stream();
        List res = stream.limit(2).collect(Collectors.toList());
        res.forEach(System.out::println);

在这里插入图片描述

5.3.6、peek

生成一个包含原Stream所有元素的新Stream,同时提供一个消费函数(Consume),当Stream中每个元素被消费(访问)时都会执行给定的消费函数。

Stream.of("one", "two", "three", "four")
//通过filter获取新Stream ,stream1
       .filter(e -> e.length() > 3)
      //返回一个包含stream1的Stream stream2,监控stream2中元素的消费
       .peek(e -> System.out.println("Filtered value: " + e))
       //通过map,获取新的Stream,stream3
       .map(String::toUpperCase)
        //返回一个包含stream3的Stream stream4,监控stream4中元素的消费
        .peek(e -> System.out.println("Mapped value: " + e))
        //开始消费(一个元素执行完所有操作之后再对下一个元素进行处理)
        //首先消费 stream2中的"three"(map(String::toUpperCase)操作),触发第一个peek,
        //然后消费 stream4中的"THREE"(collect操作),触发第二个peek
        //接着消费  stream2中的"four"...
        .collect(Collectors.toList());

结果:
在这里插入图片描述

5.3.7、sorted

sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator<? super T> comparator)可以指定排序的方式。

对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。

        IntStream intStream = IntStream.of(4, 7, 9, 11, 7, 6, 7, 5321, 342, 5);

        intStream.boxed()
                .sorted()
                .forEach(System.out::println);
5.3.8、skip

skip返回丢弃了前n个元素的流,如果流中的元素小于或者等于n,则返回空的流。

5.4、terminal操作

5.4.1、Match
public boolean 	allMatch(Predicate<? super T> predicate)
public boolean 	anyMatch(Predicate<? super T> predicate)
public boolean 	noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。

  • allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
  • anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
  • noneMatch只有在所有的元素都不满足断言时才返回true,否则flase,
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().anyMatch(n -> n > 3));  //true
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().allMatch(n -> n > 3));  //false
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().noneMatch(n -> n > 3)); //false

System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().noneMatch(n -> n > 8)); //true
System.out.println(IntStream.of(1, 2, 3, 4, 5, 6, 7).boxed().allMatch(n -> n > 0));  //true
5.4.2、count

count方法返回流中的元素的数量。它实现为:

mapToLong(e -> 1L).sum();

5.4.3、可变聚合—collect

可变聚合:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;

第一种方式:现成的收集器
<R,A> R 	collect(Collector<? super T,A,R> collector)

java8为collect方法提供了工具类Collectors,可以方便地生成List,Set,Map等集合。
Collectors是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。
例子:

Stream<String> stream1 = Arrays.asList("a", "b", "c", "a").stream();
Map<String,Long> res =stream1.collect(Collectors.groupingBy((n)->n,Collectors.counting()));
System.out.println("分组情况为:");
res.forEach((k,v)->System.out.println(k+":"+v));

在这里插入图片描述

第二种方式:定制收集器
<R> R 	collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

相当于下面的代码:

   R result = supplier.get(); 
   for (T element : this stream) accumulator.accept(result, element);
    return result;  

参数:

  • supplier 创建新结果容器的函数。
  • accumulator 一个将当元素添加到目标中的方法
  • combiner 一个将中间状态的多个任务结果整合到一起的方法(并发的时候会用到)

例子:

Stream<String> stream = Stream.of("a","b","c","d","e");
List<String> list  = stream.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
System.out.println(Arrays.toString(list.toArray()));  //abcde

而且在并行stream的collect操作中使用非并发收集器是安全的。这意味着我们下面的代码是没有线程安全的问题:

Stream<String> stream = Stream.of("a","b","c","d","e").parallel();
List<String> list  = stream.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
System.out.println(Arrays.toString(list.toArray()));  //abcde

原因是,每个任务都自己维护一个supplier提供的容器,保存当前的中间结果,当中间结果被合并时,它们在任务之间被安全地切换,并且在任何时候只有一个任务合并任何一对中间结果。
任务间结果的合并可以参考下图中的流程:
来源于https://segmentfault.com/q/1010000004944450

5.4.4、find
  • findAny()
  • findFirst()
    findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
    findFirst()返回第一个元素,如果流为空,返回空的Optional。
5.4.5、forEach、forEachOrdered
  • forEach 对于并行流管道,此操作不保证遵守流的遇到顺序,因为这样做将牺牲并行性的好处。
  • forEachOrdered 如果流具有定义的顺序,则以流的定义顺序对该流的每个元素执行操作。
5.4.6、max、min
  • max返回流中的最大值,
  • min返回流中的最小值。
5.4.7、toArray()

将流中的元素放入到一个数组中。

5.4.8、不可变聚合—reduce

除去可变聚合,剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;
reduce 操作可以实现从Stream中生成一个值,其生成的值不是随意的,而是根据指定的计算模型。
reduce方法有三个override的方法:

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

我们只看后两个方法:

pubic T reduce(T identity, BinaryOperator accumulator)

相当于:

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

参数:

  • identity 提供一个值作为计算模型的初始值(一个实例)
  • accumulator 计算模型(累积计算器)

这个方法的逻辑是,以identity作为计算模型的初始值,通过累积调用accumulator,最后得出结果。
下面是一个计算总和的例子:

Stream<Integer> stream = IntStream.of(1,3,5,7,9,12).boxed();
Integer res = stream.reduce(0,(sum, b)->sum+b);
System.out.println(res);

(sum, b)->sum+b中,sum代表上次调用计算模型的结果(初始值为identity参数值,也就是0),b代表流中的元素。

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

按照上面的方法,已经足够使用,那么这个三个参数的方法的作用是什么呢?是为了通过并行计算,提高第二个方法的效率。
参数:

  • identity 提供一个值作为计算模型的初始值
  • accumulator 计算模型(累积计算器)
  • combiner 组合器,将不同任务的计算结果合并

这个方法的逻辑是通过使用多个线程对流中的元素进行计算,最后通过combiner进行组合结果,不过这里发现identity这里有个坑,就是每个子任务使用累积计算器的时候初始值都是identity,这里可能会发生下面的问题(将初始值置为1):

Stream<Integer> stream1 = IntStream.of(1,3,5,7,9,12).boxed();
Integer res1 = stream1.reduce(1,(sum, b)->sum+b);
//38 =(1+3+5+7+9+12)
System.out.println(res1);

Stream<Integer> stream2 = IntStream.of(1,3,5,7,9,12).boxed().parallel();
Integer res2 = stream2.reduce(1,(sum,n)->sum+n,(a,b)->a+b);
//45 = ((1+1)+(1+3)+(1+5)+(1+7)+(1+9)+(1+12))
System.out.println(res2);

上面的代码中,第二个流为并行流,计算的时候,每个任务计算的时候,初始值都是1,这就导致了一个错误的结果。
所以官方文档中对identity的值的限制做了一些说明:

identity值必须是组合器功能的标识。
这意味着,对于所有u(流中的元素),combiner(identity, u)等于u 。
另外combiner功能必须兼容accumulator功能; 对于所有u和t ,以下必须保持:combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
combiner.apply(u, accumulator.apply(identity, t)) 中u代表并行下其他子任务的结果,t代表流中的一个元素
accumulator.apply(u, t) 中u代表非并行条件下的上一个计算结果,t代表流中的元素。

显然这只是并行和非并行两种不同的处理运算方式,他们应该是相同的。

按照上面的限制,我们把例子中的identity置为0,则一切都正常了。

一些最佳实践

  • 流可以从非线程安全的集合中创建,当流的管道执行的时候,非线程安全的数据源不应该被改变。例如下面的代码会抛出异常:
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));

在设置中间操作的时候,可以更改数据源,只有在执行终点操作的时候,才有可能出现并发问题(抛出异常,或者不期望的结果),比如下面的代码不会抛出异常:

List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
l.add("three");
sl.forEach(System.out::println);

对于线程安全的数据源,不会有这样的问题,比如下面的代码很正常:

List<String> l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));
  • 使用无状态的lambda。如果流操作的行为参数是有状态的,流管道结果可能不确定或不正确。 有状态的lambda(或实现适当的功能接口的其他对象)是结果取决于在流管道的执行期间可能改变的任何状态。
    例如下面的代码,可能会有问题:
public class StreamTest {

    @Test
    public void test1() {

        final State state = new State();

        List<String> l = new ArrayList(Arrays.asList("one", "two","three","four","five"));

        l.stream().map((str)->{
            if(state.flag){
                return "OK";
            }else{
                state.flag = true;
                return str;
            }
        });


    }
}

class State{
    boolean flag;
}
  • 以函数式编程的思想使用Stream。Stream整个体系设计在函数式编程的思想之上,所以我们使用的时候也应该使用函数式编程的思想来使用Stream,否则很可能出现一些副作用。

简单聊下函数式编程,实在了解的有限,现在结合自己的学习情况简单做下说明:
函数式编程的特点:
1、函数是一等公民,可以当做变量,函数的参数
2、函数式编程中的不变性,用java来解释就是所有的变量都是final的,如果需要改变状态,只能通过函数的调用
例如:
先看一个非函数式的例子:
int cnt;
void increment(){
cnt++;
}
那么,函数式的应该怎么写呢?
int increment(int cnt){
return cnt+1;
}
你可能会觉得这个例子太普通了。是的,这个例子就是函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。
3、无副作用
不会产生side effect,不需要对外部的状态产生顾虑,这样也就会避免了线程安全等一些由于状态改变引起的问题

参考:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值