Java 8 新特性——使用Stream API来处理集合

目录

1. 什么是Stream(“流”)?

官方正解“流”的概念: 流是数据的渠道。流代表了一个对象序列。流操作数据源,如数组或集合。流本身不存储数据,而只是移动数据,在移动过程中可能会对数据执行过滤,排序或其他操作。然而一般来说,流本身不修改数据源(集合,数组,I/O channel, 产生器generator ) 对流排序不会修改数据源的排序;相反,对流排序会创建一个新流,其中包含排序后的结果。
通俗理解(浅显): Stream 是元素的集合,类似于集合中的迭代器——Iterator,不过是高级版本的迭代器


理解流是什么
流是什么

从图中我们就可以鲜明的看到流是怎么工作的:

  • goods 集合提供了元素序列的数据源,通过 stream() 方法获得 Stream
  • filter / sorted / limit 进行数据处理,“连接起来” 构成 “流水线”
  • forEach 最终执行
//代码示例
 goods.stream()  
    .filter(c -> c.getPrice() > 500 && c.getSales() < 200)  
    .sorted(Comparator.comparing(Good::getPrice).reversed())  
    .limit(10)  
    .forEach(c -> { c.setPrice(c.getPrice() / 2); });  

通过对比集合中的迭代器我们会发现,流的理念更加清晰与透彻,容易理解,工作效率更高。

个人想法: 集合好比是一个储物空间(容器),什么东西(对象)都可以往里面放,当我们需要的时候就往里面取,怎么取?用标签(索引)去取,OK。那假如我们需要找出里面最特变的那个,或者从中筛选出符合某种特征一类,或者给他们分分类,排排序什么的我们该怎么办?是不是需要一遍遍的去翻找,这就很麻烦。假如我们换一种思路,我们有一个流水线(管道),在需要对集合中的元素进行操作的时候,我们就把这些元素放到这个流水线上(变成流),我们需要进行诸如过滤,排序等操作时,只需要在这条流水线上同时操作,那么工作效率将会得到很大的提高。

不留误区: 很多时候我们会有这样的误区,既然集合是用来存储对象的,那么流也是用来存储对象的,其实个人认为这种认知是不地道的。流其实是为了方便操作集合中的元素而存在的,流是一种一次性消耗品(就比如岁月如水,过了就是过了),进行了一次操作,完成了它的使命,自然也就消亡了,所以自始至终数据都还好端端的保存在数据源中(数组或集合中),这就是为什么说流本身不修改数据源

加深印象

  • 不存储数据。 流不是一个存储元素的数据结构。 它只是传递源(source)的数据。
  • 功能性的(Functional in nature)。 在流上操作只是产生一个结果,不会修改源。 例如filter只是生成一个筛选后的stream,不会删除源里的元素。
  • 延迟搜索。 许多流操作, 如filter, map等,都是延迟执行。 中间操作总是lazy的。
  • Stream可能是无界的。 而集合总是有界的(元素数量是有限大小)。 短路操作如limit(n) , findFirst()可以在有限的时间内完成在无界的stream
  • 可消费的(Consumable)。 流的元素在流的声明周期内只能访问一次。 再次访问只能再重新从源中生成一个Stream

2. Java 8 为什么引入 Stream API

  • 在使用集合中的Iterator遍历集合,完成相关聚合应用逻辑操作时效率低下,笨拙
  • 与lambda表达式结合可以对集合对象进行各种非常便利,高效的聚合操作,或者大批量的数据操作
  • 提供串行和并发两种聚合操作模式,并发能充分利用多核处理器优势,加速处理过程,利于写出高性能的并发程序。

Stream是一个函数式语言+多核时代综合影响的产物

3. 什么是聚合操作

或许第一次听说“聚合操作”这个术语时很多人都会觉得陌生,误以为这是一种多么高大上的操作。实际上,“聚合”一直广泛的应用于程序员的开发之中,比如关系型数据库中的一些操作就是“聚合操作”:

  • 客户每月平均消费金额 —— 平均值
  • 最昂贵的在售商品 —— 最值
  • 本周完成的有效订单 —— 去除特殊值
  • 取十个数据样本作为首页推荐 —— 截取一段数据

典型事例:在关系型数据库中,我们可以使用sql语句的 sum max min avg distinct 等函数实现聚合操作

加深理解: 聚合操作(也称为折叠)是接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的聚合操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有

4. 流的两种操作类型

4.1 了解原理

  • Intermediate(中间操作): 中间操作可以用来执行一系列动作的管道。一个流后面跟随零个或多个中间操作(Intermediate),主要目的是为了打开流,做出某种程度的数据映射(过滤),然后返回一个新的流,交给下一个操作使用。这类操作是惰性化的(lazy),也就是说,仅仅调用这类方法,并没有真正开始流的遍历。换句话说,中间操作不是立即发生的,相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生——这种机制称为延迟行为,所以中间操作是延迟发生的,延迟行为让流 API 能更加高效地执行。
  • Terminal(终端操作): 终端操作会消费流,该操作用于产生结果,例如找出流中的最值。一个流中只能有一个terminal操作,当这个操作执行完成后,流就被消费光了,表示流已经死亡,无法使用。所以终端操作必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。
  • short-circuiting(短路操作): 尽早结束对流的操作,不必检查所有元素
    • 对于一个Intermediate操作,接受一个无限大的流(infinite/unbounded)的Stream,返回一个有限的Stream。
    • 对于一个Terminal操作,接受一个无限大的Stream,但能在有限的时间中计算出结果。

深度剖析,不留遗憾:
在中间操作原理讲解中,有一句话标注了黑体。如果我们细心一点的话也许会发现一个问题:终端操作是一个遍历流的过程,意味着流的死亡,那为什么还说中间操作是在其创建的流在执行终端操作后才执行?不是说流被消耗后就不能使用了吗?为什么这种延迟执行反而提高了效率呢?难道这句话有矛盾?其实,这句话是绝对正确的,且听我娓娓道来:

  • 延迟执行的效率:其实,在原理中解释的已经很清楚了,中间操作是 lazy 的,多个中间操作(诸如排序,过滤等)只会在Terminal操作的时候融合起来,一次循环完成。简单理解为,Stream里有个操作函数的集合,每次中间操作就把转换函数放入到这个集合中,在Terminal操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。(想想生活中的例子,建筑工地施工,是等材料到齐了开工效率高还是来了一部分材料就开工效率高)
  • 终端操作执行完后执行中间操作:或许理解了延迟执行也就理解了这一点,就不过多赘述了。你只要清楚一点,效率至上,谁先执行,谁后执行,效率说了算。

代码示例

int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum()

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

4.2 两种操作方法分类

  • Intermediate:
    map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:
    forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:
    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

5. 流的使用

5.1 几个重要的流接口概要

流 API 的定义的接口包含在 java.util.stream 中。BaseStream 是基础接口,它定义了所有流都可以使用的基本功能,声明如下

interface BaseStream<T, S extends BaseStream<T, S>>
T 指定流元素的类型,S 指定扩展了 BaseStream 的流的类型。
思考:既然是流,是否需要关闭?
回答:只有当使用的数据源需要关闭时(如流连接到文件),才需要关闭流。大多数时候,例如数据源是集合或数组时,不需要关闭流。

BaseStream 下派生出的接口

 1.interface Stream<T> //对象流,使用最为普遍,操作的是对象的引用,本章知识重点
 
 //如下几个流是对 BaseStream 的扩展,类似于 Stream,只是操作的是基本类型
 2.interface DoubleStream
 3.interface IntStream
 4.interface LongStream

5.2 获取流的几种方式

5.2.1 从集合获取流

从集合中获取流是最为普遍的做法:主要 stream() 和 paralleStream()方法

// 从 Collection 中获取流
List<People> list = new ArrayList<People>();
list.add(new people("xiaoming", 12));
list.add(new people("xiaohua", 15));
list.add(new people("xiaogan", 18));
            ···
stream = list.stream();
5.2.2 从数组获取流

流不但可以从集合获取,还可以直接从数组中获取流,Java 8 为Arrays类添加了 stream() 方法获取流
Stream类的静态工厂方法: Stream.of(Object[]), IntStream.range(int, int), Stream.iterate(Object, UnaryOperator);

//1. 调用静态的 of()方法 返回指定元素的顺序排序流
 Stream stream = Stream.of("a", "b", "c")

//2. 调用 Arrays 类中的 stream() 方法从数组中获取一个顺序流
String[] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);

5.2.3 其他获取流的方法(了解)
  • 文件行 BufferedReader.lines()
  • Files类的获取文件路径列表: find(), lines(), list(), walk()
  • Random.ints() 随机数流, 无界的
  • BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence),JarFile.stream()
  • 通过StreamSupport辅助类从spliterator产生流

5.3 流转换为其他数据结构

我们可以把数组或者集合转换为流,同样的也可以把流转换为数组或者集合。

5.3.1 将流装换为数组
String[] strArray = new String[]{"asd", "wer", "wey", "ppu"}
Stream stream = Stream.of(strArray)
        ···
String[] strArry1 = stream.toArray(String[]::new);

5.3.2 将流转换为集合(收集——collect方法的使用)

将流转换为集合的过程称为收集(可以理解为从管道中捞取),这时我们需要使用一个很重要的方法 collect(),定义如下:

//该方法是Stream中的终端方法,用于返回一个集合(各种集合)
<R,A> R collect(Collector<? super T,A,R> collector)
R 指定结果的类型,T 指定调用流的元素类型, A 指定一个可变的累积类型(一个累积的容器)

在Java API中为我们提供了一个 Collectors 工具类,里面提供了关于流操作的一些静态方法,如 toList(),toMap(),toSet(),toCollection() 等将流装换为集合的方法

String[] strArray = new String[]{"asd", "wer", "wey", "ppu"}
Stream stream = Stream.of(strArray)
        ···
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));

5.3.3 将流转换为字符串
String[] strArray = new String[]{"asd", "wer", "wey", "ppu"}
Stream stream = Stream.of(strArray)
        ···
String str = stream.collect(Collection.joining()).toString();

5.4 中间操作(转换操作)解析

中间操作在流的使用中非常频繁,如下是几个常用的转换方法:

  • filter: 对流中的元素使用给定的过滤函数进行过滤操作,新生成的流中只包含符合条件的元素
    filter示意图如下:
    filter示意图

  • map(): 对流中包含的元素使用给定的转换函数进行转换操作,新生成的流只包含转换生成的元素。该方法有三个变种方法:mapToInt(),mapToLong(),mapToDouble()
    map 示意图如下:
    map示意图

  • flatMap(): 类似于map(),不过该方法是一对多的替换元素,并将替换的元素平坦的化为新流
    flatMap示意图如下:
    flatMap示意图

  • distinct: 对流中包含的元素进行去重操作(去除重复的元素),新生成的流中将没有重复的元素
    distinct示意图如下:
    distinct示意图

  • peek: 返回由该流的元素组成的流,另外在消耗流中的每个元素时执行提供的操作。该方法主要用于调试使用
    peek示意图如下:
    peek示意图


代码示例

Stream.of("one", "two", "three", "four") .filter(e -> e.length() > 3) .peek(e -> System.out.println("Filtered value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList());  

  • limit: 对流进行截断操作,获取流中的前N个元素,如果流中的元素个数小于N,则获取所有的元素
    limit示意图如下:
    limit示意图

  • skip: 与limit的效果相反。返回一个丢弃原Stream的前N个元素后剩下元素所组成的新流,如果原Stream中包含的元素个数小于N,那么返回一个空Stream
    skip示意图如下:
    skip示意图


代码示例 (综合了上面的方法)

List<Integer> nums = new ArrayList<>();
          nums.add(1);
          nums.add(null);
          nums.add(2);
          nums.add(1); 
          nums.add(4);
          nums.add(3);
          nums.add(null);
          nums.add(5);
          nums.add(6);
          nums.add(7);
          nums.add(8);
          nums.add(9);

          System.out.println("sums is : " + nums.stream().filter(num -> num != null).distinct()
                  .mapToInt(num -> num * 2).
                          peek(System.out::println).skip(2).limit(4).sum());

5.5 终止(汇聚)操作解析

  • anyMatch / allMatch / noneMatch
    allMatch:Stream 中全部元素符合传入的 predicate,返回 true
    anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
    noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true
List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream().
 allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().
 anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);
  • count / min / max
    count:得到元素的数量
    max:取得元素中的最大值
    min:取得元素中的最小值

  • findAny / findFirst /forEach
    findAny:返回流中的任意一个元素
    findFirst:返回流中的第一个元素
    forEach:遍历流

5.6 使用流的基本步骤

也许听了上面关于中间操作,终端操作会有点懵,在自己写代码的时候可能感觉无从下手,或许你就可以按照下面的步骤了

  • 创建流(Stream)
  • 中间操作(转换Stream),每次转换原有的Stream对象不变,返回一个新的Stream对象
  • 终端操作,获取结果

Stream通用语法剖析示意图:
Stream通用语法剖析示意图

5.7 流的迭代器

虽然流不是数据存储对象,但是仍然可以使用迭代器来遍历元素,就如同使用迭代器遍历集合中的元素一样。流 API 支持两类迭代器,一类是传统的 Iterator,另一类是 JDK 8新增的 Spliterator.

  • 使用Iterator
List<String> nums = new ArrayList<>();
         nums.add("sdk");
         nums.add("ffg");
         nums.add("kko"); 
         nums.add("mmp");
         nums.add("kkl");
         
         Stream<String> myStream = nums.stream();
         
         Iterator<String> itr = myStream.iterator();
         
         while(itr.hasNext()){
           System.out.println(itr.next());
         }
  • 使用 Spliterator : 可以替代 Iterator,在涉及并行处理时更加方便。
List<String> nums = new ArrayList<>();
          nums.add("sdk");
          nums.add("ffg");
          nums.add("kko"); 
          nums.add("mmp");
          nums.add("kkl");
          
          Stream<String> myStream = nums.stream();
          
          Spliterator<String> splitItr = myStream.spliterator();
          
          while(splitItr.tryAdvance((n) -> Syatem.out.println(n)));
          splitItr.forEachRemain((n) -> Syatem.out.println(n)))
          
          关于tryAdvance
          定义:boolean tryAdvance(Comsumer<? super T> action)
          action 指代了在迭代器中的下一个元素上执行的操作。如果有下一个元素,tryAdvance()返回true,否则返回false

参考书籍

疯狂Java讲义
Java 8编程参考官方教程

参考文档

http://www.importnew.com/20331.html
http://www.importnew.com/16545.html
http://www.iteye.com/news/32782
https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值