Java8专题四《引入流》

目录

1、 什么是流?

2、使用流的好处

3、集合和流

4、流操作的分类

4.1  中间操作

4.2  终端操作

4.3  使用流的步骤

5、 小结


1、 什么是流?

流是JavaAPI的新成员,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码!我们会在后面的章节详细解释流和并行化是怎么工作的。

2、使用流的好处

用一个例子展示流的好处,下面两段代码都是用来返回低热量的菜肴名称的,按照卡路里排序。

//Java8之前  
List<Dish> lowCaloricDishes = new ArrayList<>();
        for (Dish d : menu) {
            if (d.getCalories() < 400) {
                lowCaloricDishes.add(d);
            }
        }
        Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
            public int compare(Dish d1, Dish d2) {
                return Integer.compare(d1.getCalories(), d2.getCalories());
            }
        });
        List<String> lowCaloricDishesName = new ArrayList<>();
        for (Dish d : lowCaloricDishes) {
            lowCaloricDishesName.add(d.getName());
        }
//之后
import static java.util.Comparator.comparing ; 
      import static java.util.stream.Collectors.toList;
      
        List<String> lowCaloricDishesName =
                menu.stream()
                        .filter(d -> d.getCalories() < 400)
                        .sorted(comparing(Dish::getCalories))
                        .map(Dish::getName)
                        .collect(toList());
//为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream();
List<String> lowCaloricDishesName = 
 menu.parallelStream() 
 .filter(d -> d.getCalories() < 400) 
 .sorted(comparing(Dishes::getCalories)) 
 .map(Dish::getName) 
 .collect(toList()); 

比较:

Java8之前你用了一个“垃圾变量”lowCaloricDishes,它唯一的作用就是作为一次性的中间容器。
Java8之后,你的代码有以下特点:

  • 声明性。(更简洁,更易懂)
  • 可复合。(更灵活)
  • 可并行。(性能更好)

(你可能会想,在调用parallelStream方法的时候到底发生了什么?用了多少个线程?对性能有多大的提升,我们也会在后面的专题详细讨论这些问题)

3、集合和流

粗略的说,集合和流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所以的值---集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西)
相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素是按需计算的。
java8的集合就像是存在DVD上的电影,Java8中的流就像用在线流媒体看的电影。

1)只能遍历一次

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。例如以下代码会抛出一个异常,说流已被消费掉了。

List<String> title = Arrays.asList("Java8", "In", "Action"); 
Stream<String> s = title.stream(); 
s.forEach(System.out::println); 
s.forEach(System.out::println);

所以要记得,流只能消费一次

 2) 内部迭代与外部迭代
集合和流的另一个关键区别在于它们遍历数据的方法。

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Stream库使用内部迭代---它帮你把迭代做了,还把得到的流存在了某个地方,你只要给出一个函数说要干什么就可以了。下面的代码列表说明了这种区别。

集合:用for-each循环外部迭代

 List<String> names = new ArrayList<>();
        for (Dish d : menu) {
            names.add(d.getName());
        }

//集合:用背后的迭代器做外部迭代
Iterator<String> iterator = menu.iterator();
        while (iterator.hasNext()) {
            Dish d = iterator.next();
            names.add(d.getName());
        }

流:内部迭代

List<String> names = menu.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

所以,集合和流的另一个关键区别在于它们遍历数据的方法。

4、流操作的分类

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分成两大类。我们来看一下前面的例子:

List<String> names = menu.stream()
                .filter(d -> d.getCalories() > 300)
                .map(Dish::getName)
                .limit(3)
                .collect(toList());

你可以看到两类操作:
1. filter、map和limit可以连成一条流水线。
2. collect触发流水线执行并关闭它。
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。

 

4.1  中间操作

诸如filter或者sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的菜肴。

 List<String> names =
                menu.stream()
                        .filter(d -> {
                            System.out.println("filtering" + d.getName());
                            return d.getCalories() > 300;
                           
                        })
                        .map(d -> {

                            System.out.println("mapping" + d.getName());
                            return d.getName();
                        })
                        .limit(3)
                        .collect(toList());
        System.out.println(names);

//结果
filtering pork 
mapping pork 
filtering beef 
mapping beef 
filtering chicken 
mapping chicken 
[pork, beef, chicken] 

你会发现,有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧,我们会在下一专题中解释。第二,尽管fifter和map都是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫做循环合并)。

4.2  终端操作

终端操作会把流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至是void。例如下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。

menu.stream().forEach(System.out::println); 

4.3  使用流的步骤

总而言之,流的使用一般包括三件事:
1. 一个数据源(如集合)来执行一个查询;
2. 一个中间操作链,形成一条流的流水线;
3. 一个终端操作,执行流水线,并能生成结果。
流的流水线背后理念类似于构建器模式。在构建起模式中有一个通用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。
下图总结了你前面在代码例子中看到的中间流操作和终端流操作。这并不能涵盖Stream API提供的操作,你会在下一章节看到更多。

5、 小结

以下是你应从本章中学到的一些关键概念。

  1. 流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了。
  2. 流操作有两类:中间操作和终端操作。
  3. 流操作有两类:中间操作和终端操作。
  4. filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果。
  5. forEach和count等终端操作会返回一个非流的值,并处理流水线以返回结果。
  6. 流中的元素是按需计算的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值