如果说前面几章是函数式编程的方法论,那么 Stream 流就应该是 JAVA8 为我们提供的最佳实践。

Stream 流的定义

Stream 是支持串行和并行操作的一系列元素。流操作会被组合到流管道中(Pipeline)中,一个流管道必须包含一个源(Source),这个源可以是一个数组(Array),集合(Collection)或者 I/O Channel,会有一个或者多个中间操作,中间操作的意思就是流与流的操作,流还会包含一个中止操作,这个中止操作会生成一个结果。

Stream 流的作用

以函数式编程的方式更好的操作集合。完全依赖于函数式接口。在 java.util.stream 包中。

流的创建方式

  • 使用数组的方式

    //第一种方式,使用 Stream.of 方法
    Stream stream1 = Stream.of("hello","world","hello world"); String[] myArray = new String[]{"hello","world","hello world"};
    Stream stream2 = Stream.of(myArray); //第二种方式,使用 Arrays.stream()
    Stream stream3 = Arrays.stream(myArray);
  • 使用集合的方式

    Stream 的作用是以函数式编程的方式操作集合,所以对于集合类,一定有更好更方便的方法去创建 Stream 流。

    List<String> list = Arrays.asList(myArray);
    Stream stream4 = list.stream();

    对于集合类,直接调用 stream 方法就可以获得这个集合对应的 Stream 流。通过查看源码我们发现这个方法直接定义在 Collection 接口中,并且是一个默认方法。所以所有 Collection 的子类都可以直接调用这个方法。这也是最为常用的方法。

    default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
    }
  • 使用文件流(基本不会使用,简单了解即可)

    下面是一个直接读取文件中的内容并且转化为 Stream 流,最后输出的过程。

    //文件流
    private static Stream<String> fileStream(){
    Path path = Paths.get("C:\\Users\\abs\\a.txt");
    try(Stream<String> lines = Files.lines(path)){
    lines.forEach(System.out::println);
    return lines;
    }catch(IOException e){
    throw new RuntimeException(e);
    }
    }
  • 其他方式

    最后的方式是用于创建无限流,无限流的意思是如果你不加任何限制,流中的数据是无限。用于创建无限流的方法有 iterator 和 generate。

    generate 方法需要传入一个 Supplier 类型的函数式接口,这个函数式接口用于产生无限流中所需要的数据。

    //全是数字 1 的无限流
    Stream.generate(()->1);
    //随机数字的无限流
    Stream.generate(Math::random);

    iterator 方法需要传入两个参数,第一个给定一个初始值,第二个参数是一个函数式接口 UnaryOperator,这个函数式接口就是输入和输出相同的 Function 接口。

    Stream.iterate(0,n->n+1).limit(10).forEach(System.out::println);

    输出结果:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

    上面的 limit 是避免无限流一直产生,到达指定个数就停止。

Steam 流的优势

下面我们通过一个简单的例子来了解一下使用 Stream 流到底有哪些好处。等我们学完 Stream API 后会给大家提供更多的例子,让大家真正了解它。

比如我们给定一个 List 集合,里面放了很多数字,我们想要得到数字的平方然后求和。

以前的写法:

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
int res = 0;
for(int i=0;i<l.size();i++){
res += i*i;
}

使用 Stream 后的写法只需要一行代码:

int r = l.stream().map(i->i+i).reduce(0,Integer::sum);

大家现在可能不明白 map 或者 reduce 的作用,我们稍后会详细讲解这一部分,这里只是想让大家看看区别,以及认识到 Stream 对于函数式编程的使用和好处。

Stream 流的特性和原理

流不存储值,通过管道的方式获取值。对流的操作会生成一个结果,不过并不会修改底层的数据源。集合可以作为流的底层数据源,也可以通过 generate/iterator 方法来生成数据源。

得到流之后,我们可以对流中的数据进行很多操作,比如过滤,映射,排序等等,处理完之后的数据可以再次被收集起来转化为我们需要的数据类型。

从上面的图我们可以看出,一个完成的流操作过程是包含两种类型的,一个是中间操作,一个是终止操作。中间操作指的是过滤,排序和映射等中间处理过程的方法,终止操作指的是我们将流处理完毕后返回结果的操作,比如 collect,reduce 和 count 等等。

中间操作:一个流后面可以跟随零个或者多个中间操作。其目的只要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用,这些操作都是延迟的,就是说仅仅调用到这些类的方法,并没有真正开始流的遍历。

终止操作:一个流只能有一个终止操作,当这个操作执行后,流就被使用光了,无法再被操作。所以这必定是流的最后一个操作。终止操作的执行,才会是真正开始流的遍历,并且会生成一个结果。

还有一个重要的概念流是惰性的在数据源上的计算只有数据在被终止的时候才会被执行。也就是说所有的中间操作都是惰性求值,不遇到终止操作,中间操作的代码是不会执行的。

举个例子:

我们对于一个数字结合进行一个 map 中间操作,将元素乘以 2,同时我们有一个 System.out 语句用于查看代码是否执行了。

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
l.stream().map(i->{
i = i*2;
System.out.println(i);
return i;
});

最终的执行结果是什么也没打印。

那如果我们给他加一个终止操作那,结果如下:

中间操作:2
终止操作:2
中间操作:4
终止操作:4
中间操作:6
终止操作:6
中间操作:8
终止操作:8
中间操作:10
终止操作:10
中间操作:12
终止操作:12
中间操作:14
终止操作:14

这时中间操作和终止操作都执行了,这证明中间操作是惰性的。

还有一个需要注意的点就是,Stream 其实与 IO Stream 的概念是一致的,是不能重复使用的,关闭(执行终止操作后就关闭了)后也是不能使用的。

//用集合生成一个流并进行过滤,过滤后返回一个 stream s1。
List<Integer> l = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream s1 = l.stream().filter(item -> item > 2); //因为 filter 是中间操作,流并没有被关闭,所以还可以执行其他操作,distnict 是一个终止操作,执行完毕后流就关闭了
s1.distinct(); //流已经关闭了,再执行操作就会抛出异常
s1.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at com.paul.framework.chapter7.StreamCreate.main(StreamCreate.java:48)

Stream 流的 API 采用了建造者设计模式,这就意味着我们可以在一句代码中连续调用 Stream 的 API。

中间操作 API

  • filter

    顾名思义,filter 就是过滤的意思。参数需要我们传入一个 Predicate 类型的函数式接口。不符合 Predicate 函数式接口的条件的流将被过滤出去。

    Stream<T> filter(Predicate<? super T> predicate);

    我们需要筛选出分数大于 60 分的学生:

    public static void main(String[] args) {
    List<Student> lists = new ArrayList<>();
    lists.add(new Student("wang",80,"Female"));
    lists.add(new Student("li",95,"Male"));
    lists.add(new Student("zhao",100,"FeMale"));
    lists.add(new Student("qian",54,"Male")); // filter 是一个中间操作,返回过滤后的 Stream 流。forEach 是一个终止操作,对 filter 过滤之后的流进行处理。
    lists.stream().filter(s->s.getMark()>60).forEach((s)-> System.out.println(s.getName()));
    } //上一个例子我们对过滤后的流进行了打印操作,我们其实也可以把过滤后的流整理成一个集合
    List<Student> l = lists.stream().filter(s->s.getMark()>60).collect(Collectors.toList());
    l.forEach(s-> System.out.println(s.getName()));

    两次打印的结果是相同的:

    //第一次打印的结果
    wang
    li
    zhao //第二次打印的结果
    wang
    li
    zhao

    filter 函数为我们提供了最为简单的方法去过滤集合,避免了重复代码,逻辑也更易懂。

  • map

    通过流的方式对集合中的元素进行匹配操作。参数需要我们传入一个 Function 类型的函数式接口。Function 类型的函数式接口需要一个输入和一个输出,对应映射之前需要的元素和映射之后得到的元素。

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

    比如集合中的元素是学生类,我们最终想要得到的结果是学生的分数,就可以使用 map 方法。

    List<Student> lists = new ArrayList<>();
    lists.add(new Student("wang",80,"Female"));
    lists.add(new Student("li",95,"Male"));
    lists.add(new Student("zhao",100,"FeMale"));
    lists.add(new Student("qian",54,"Male"));
    lists.stream().map(s->s.getMark()).collect(Collectors.toList()).forEach(System.out::println);

    将 list 转换为 stream 后,通过 map 方法将学生类转换成学生成绩的 int 类型,然后通过 collect 方法将流转换为集合,最后通过 forEach 方法将学生成绩打印出来。

    打印的结果:

    80
    95
    100
    54

    比如我们想将集合中的字符串转换成大写字母。

    List<String> list = Arrays.asList("hello","world","helloworld","test");
    list.stream().map(String::toUpperCase).collect(Collectors.toList()).forEach(System.out::println);

    map 方法里我们通过方法引用(将字符串转为大写的方法 String 类已经定义好了,所以我们直接使用方法引用,而不是写一个匿名函数的 Lambda 表达式)将集合中的字符串转换成大写,然后通过 collect 将流转换为集合,最终使用 forEach 方法打印转换后的字符串。

    打印结果:

    HELLO
    WORLD
    HELLOWORLD
    TEST
  • mapTo*

    如果我们的 map 方法返回值是 int,long 或者 double 的话,我们可以直接使用 Stream API 为我们提供了 mapToInt,mapToLong,mapToDouble 方法。这几个方法返回的是 IntStream,LongStream 和 DoubleStream。

    mapToInt, mapToLong 和 mapToDouble 是为了避免自动拆装箱带来的性能损耗。大家应该知道,像 int,long,double 这种基本数据类型是不能使用面向对象相关操作的,为此 Java 引入了自动拆装箱的功能,能够在需要使用面向对象的特性时帮我们将基本数据类型 int,long 和 double 转换为 Integer,Long 和 Double 等包装类型。在需要使用基本数据类型时(比如计算),又可以将包装类型 Integer,Long 和 Double 转换为基本数据类型 int,long 和 double。

    如果我们使用的不对,就会有一些自动拆装箱的性能损耗。

    如果我们需要得到基本数据类型的结果,就可以使用 mapToInt, mapToLong 和 mapToDouble,这样的到的是基本数据类型的流,可以方便我们进行计算等等操作。

    int sum = lists.stream().mapToInt(s->s.getMark()).sum();
    System.out.println(sum);
  • flatMap

    flat 的意思是扁平化,这个函数式的作用是将我们 map 之后的集合或者数组等等元素打散成我们想要的元素。

    flatMap 方法需要返回一个 Stream 数据类型。T 是输入的集合类型元素,R 是打散之后的元素类型,是 Stream 类型。

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

    来看一个例子:

    比如我们的流中以前有三个 ArrayList,map 之后依然后会有三个 ArrayList,flatMap 会将三个 ArrayList 合并到一个 ArrayList 中。

    Stream<List<Integer>> stream = Stream.of(Arrays.asList(1),ArrayList.asList(2,3), ArrayList.asList(4,5,6));
    
    // 将 stream 里面的每一个 list 再次转化为 stream<Integer>,然后在进行 map 操作。
    stream.flatMap(theList->theList.stream()).map(item->item*item).forEach(System.out::println);

    这个例子中,List 代表打散之前的元素,Integer 代表我们打散之后的元素类型。

    在看另外一个例子,字符串去重复。

    List<String> list = Arrays.asList("hello welcome","world hello","hello world hello","hello welcome");
    
    //错误的写法, 这是对 String[] 的 distinct
    List<String[]> result = list.stream().map(item->item.split(" ")).distinct().collect(Collectors.toList());

    split 方法输入的是字符串,返回的是一个字符串数组,所以最后返回的是 String 数组流 Stream<String[]>。

    我们使用 flatMap 将 String 数组打散成 String。

    //正确的写法,要用 flatmap 将 String[] 打散成 String
    List<String> result = list.stream().map(item->item.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());

  • flatMapTo*

    flatMapTo* 也有许多具体的实现实现,和 mapTo* 用法类似,这里就不再赘述了。

  • limit

    limit 方法可以对流中需要返回的元素加以限制,因为流中元素的方法执行是严格按照顺序进行的,limit 方法就相当于取前几个元素。

    我们通过下面这个例子来了解 limit 和无限流。

    IntStream.iterate(0, i->(i+1)%2).distinct().limit(6).forEach(System.out::println);

    IntStream.iterate(0, i->(i+1)%2) 不断产生 0,1,0,1,0,1..... 这样的无限流,distinct 方法去除重复,limit 方法虽然限制流中只有 6 个元素,但是 distinct 方法先执行它会对无限流一致执行去复操作,所以方法永远不会结束。这个 limit 在这里也失去了作用。

    执行结果虽然只显示了 0,1。但是方法一直不会结束。

    正确的写法:

    IntStream.iterate(0, i->(i+1)%2).limit(6).distinct().forEach(System.out::println);

    先调用 limit 方法,限制流中只有 6 个元素,然后去重,结果打印 0,1。程序结束。

  • skip

    skip 方法和 limit 方法的用法类似,可以跳过流中的前几个元素。

    IntStream.iterate(0, i->i+1).limit(10).skip(3).forEach(System.out::println);

    首先通过 iterate 和 limit 产生 10 Integer 个元素的流,通过 skip 跳过前三个。最终的结果如下:

    3
    4
    5
    6
    7
    8
    9
  • sort

    sort 方法有两个实现,一个是不需要传入参数的,另一个是需要我们传入 Comparator。

    //根据自然顺序排序
    Stream<T> sorted();
    //根据 Comparator 的规则进行排序
    Stream<T> sorted(Comparator<? super T> comparator);

    我们以前对集合排序时通常会使用 JDK 中 Collection 接口的 sort 方法:

    List<String> names = Arrays.asList("java8","lambda","method","class");
    
    //以前的写法
    Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
    return o2.compareTo(o1);
    }
    });

    使用 Lambda 表达式对上面的写法进行一下改进:

    Collections.sort(names,(o1,o2)-> o2.compareTo(o1));

    现在我们还可以使用 Stream API 中的 sorted 方法:

    names.stream().sorted().forEach(System.out::println);
    names.stream().sorted((o1,o2)-> o2.compareTo(o1)).forEach(System.out::println);

    结果:

    class
    java8
    lambda
    method

JAVA8之 Stream 流(四)的更多相关文章

  1. java8之Stream流处理

    简介 Stream 流处理,首先要澄清的是 java8 中的 Stream 与 I/O 流 InputStream 和 OutputStream 是完全不同的概念. Stream 机制是针对集合迭代器 ...

  2. Java8的Stream流(一) --- 基础用法

    Java8中的Stream Stream使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象. Stream的特性及优点: 无存储. Stream不是一种数据 ...

  3. 这可能是史上最好的 Java8 新特性 Stream 流教程

    本文翻译自 https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ 作者: @Winterbe 欢迎关注个人微信公众 ...

  4. 【Java8新特性】- Stream流

    Java8新特性 - Stream流的应用 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! ...

  5. Java 8创建Stream流的5种方法

    不知不觉间,Java已经发展到13了,来不及感慨时间过得真的太快了,来不及学习日新月异的技术更新,目前大多数公司还是使用的JDK8版本,一方面是版本的稳定,另一方面是熟悉,所以很多公司都觉得不升级也挺 ...

  6. Java Stream流排序null以及获取指定条数数据

    Java8的Stream流的一些用法, //排序 carerVehEntityList = carerVehEntityList.stream().sorted( Comparator.compari ...

  7. 乐字节-Java8新特性-接口默认方法之Stream流(下)

    接上一篇:<Java8新特性之stream>,下面继续接着讲Stream 5.流的中间操作 常见的流的中间操作,归为以下三大类:筛选和切片流操作.元素映射操作.元素排序操作: 操作 描述 ...

  8. 【转】Java8 Stream 流详解

      当我第一次阅读 Java8 中的 Stream API 时,说实话,我非常困惑,因为它的名字听起来与 Java I0 框架中的 InputStream 和 OutputStream 非常类似.但是 ...

  9. java8 新特性Stream流的应用

    作为一个合格的程序员,如何让代码更简洁明了,提升编码速度尼. 今天跟着我一起来学习下java 8  stream 流的应用吧. 废话不多说,直入正题. 考虑以下业务场景,有四个人员信息,我们需要根据性 ...

随机推荐

  1. list extend 和 append

    append 一次追加一个列表 extend 一次追加所有的元素 单个的形式加入

  2. 输出Excel文件

    /** * * 功能描述: <br> * 〈功能详细描述〉输出excle * * @param titles 标题 * @param contents 内容 * @param fileNa ...

  3. 学linux内核与学linux操作系统有什么区别!?

    linux内核包括:进程管理,存储管理,IO管理,文件系统等功能.linux操作系统则是linux内核再加上像shell或图形界面和其他的实用软件,比内核庞大的多.建议先学shell命令和linux下 ...

  4. 学习HTML<audio>标签

    首先来看下这个例子: <audio controls autoplay="autoplay"> <source src="horse.ogg" ...

  5. Oracle/PLSQL存储过程详解

    原文链接:https://blog.csdn.net/zezezuiaiya/article/details/79557621 Oracle/PLSQL存储过程详解 2018-03-14 17:31: ...

  6. Redis在Laravel项目中的应用实例详解

    https://mp.weixin.qq.com/s/axIgNPZLJDh9VFGVk7oYYA 在初步了解Redis在Laravel中的应用 那么我们试想这样的一个应用场景 一个文章或者帖子的浏览 ...

  7. CSS中的“>”是什么意思

    #quickSummary p{color:red;} #quickSummary >p+p{color:red;} #quickSummary>p+p+p{color:inherit;} ...

  8. element-ui css 文件加载 失败(https://unpkg.com/element-ui/lib/theme-chalk/index.css,加载失败)

    在main.js文件引入   import Vue from 'vue'; import Element from 'element-ui'; import 'element-ui/lib/theme ...

  9. H3C 帧中继显示与调试(续)

  10. 2019-9-2-给博客添加rss订阅

    title author date CreateTime categories 给博客添加rss订阅 lindexi 2019-09-02 12:57:38 +0800 2018-2-13 17:23 ...