(二)流(Stream)

一、引入流(Stream)

1、什么是流

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

假设有这么一个需求,要从菜单中选出热量小于400的并排序,输出菜名,下面介绍两种做法,方法一是传统方式,方法二是使用java8方式

/**
 * 菜单类
 */
@Data
@AllArgsConstructor
public class Dish {
    private String name;
    private boolean vegetarian;
    private int calories;
    private Type type;

    public enum Type {MEAT, FISH, OTHER}
}
public class DishService {
    public static void main(String[] args) {
        test1();
        test2();
    }

    /**
     * 传统方式
     */
    private static void test1() {
        //返回低热量的菜肴名称的,并按照卡路里排序
        List<Dish> menu = init();
        List<Dish> lowCaloricDishes = new ArrayList<>();
        //筛选出低于400的
        for (Dish dish : menu) {
            if (dish.getCalories() < 400) {
                lowCaloricDishes.add(dish);
            }
        }
        //排序
        Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
            @Override
            public int compare(Dish o1, Dish o2) {
                return Integer.compare(o1.getCalories(),o2.getCalories());
            }
        });
        //获取菜名
        List<String> lowCaloricDishesName = new ArrayList<>();
        for (Dish lowCaloricDish : lowCaloricDishes) {
            lowCaloricDishesName.add(lowCaloricDish.getName());
        }
        System.out.println(lowCaloricDishesName);
    }

    /**
     * java8方式
     */
    private static void test2() {
        //返回低热量的菜肴名称的,并按照卡路里排序
        List<Dish> menu = init();
        List<String> collect = menu.stream().filter(m -> m.getCalories() < 400)
                .sorted(Comparator.comparing(Dish::getCalories))
                .map(Dish::getName)
                .collect(Collectors.toList());
        System.out.println(collect);
    }

    private static List<Dish> init() {
        List<Dish> menu = Arrays.asList(
                new Dish("猪肉", false, 800, Dish.Type.MEAT),
                new Dish("牛肉", false, 700, Dish.Type.MEAT),
                new Dish("鸡肉", false, 400, Dish.Type.MEAT),
                new Dish("白菜", true, 530, Dish.Type.OTHER),
                new Dish("菠菜", true, 350, Dish.Type.OTHER),
                new Dish("时令水果", true, 120, Dish.Type.OTHER),
                new Dish("黄瓜", true, 550, Dish.Type.OTHER),
                new Dish("对虾", false, 300, Dish.Type.FISH),
                new Dish("三文鱼", false, 450, Dish.Type.FISH));
        return menu;
    }
}
从上面代码中有几个显而易见的好处:
1、代码是以声明性方式来写的:说明想要完成什么而不是说明如何实现一个操作(利用循环和if条件等控制语句)。
2、可以把几个基础操作链接起来来表达复杂的数据处理流水线(在filter后面接上 sorted、map和collect操作)同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,后传给collect方法。

(二)流(Stream)

filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构!在实践中,这意味着你用不着为了让某些数据处理任务并行而去操心线程和锁了,Stream API都替你做好了!
Java 8中的Stream API好处:
  • 声明性——更简洁,更易读
  • 可复合——更灵活
  • 可并行——性能更好

2、流简介

1、简短的定义:从支持数据处理操作生成的元素序列
元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如前面见到的 filter、sorted和map。集合讲的是数据,流讲的是计算。
源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
2、流操作的两个重要特点
流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
private static void test2() {
    List<Dish> menu = init();
    List<String> collect = menu.stream()//从menu获得流
        .filter(m -> m.getCalories() > 300)//建立操作流水线
        .map(Dish::getName)
        .limit(3)
        .collect(Collectors.toList());//将结果保存到另一个List中
    System.out.println(collect);
}
在上面代码中,先是对menu调用stream方法,由菜单得到一个流。数据源是菜肴列表(菜 单),它给流提供一个元素序列。接下来,对流应用一系列数据处理操作:filter、map、limit 和collect。除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流 水线,于是就可以看作对源的一个查询。最后,collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个List)。在调用collect之前,没有任何结果产生,实际上根本就没有从menu里选择元素。可以这么理解:链中的方法调用都在排队等待,直到调用collect。

(二)流(Stream)

filter:接受一个Lambda,从流中排除某些元素。
map:接受一个Lambda,将元素转换成其他形式或提取信息。
limit:截断流,使其元素不超过给定数量。
collect:将流转换为其他形式。可以把collect看作能够接受各种方案作为参数,并将流中的元素累积成为一个汇总结果的操作。

3、流与集合

1、集合是一个内存中的数据结构,包含数据结构中目前所有的值,也就是说集合中的值都要先计算好才能够放入集合中,但是流则不同,流是概念上固定的数据结构其元素是按需计算的,不能添加或者删除元素,只有在需要的时候才将需要的流计算出来。集合需要提前将值全部准备好而流则是将值准备一部分。
2、和迭代器类似,流只能遍历一次。遍历完之后,这个流已经被消费掉了。 你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样。遍历多次会抛出stream has already been operated upon or closed异常。
3、集合和流的一个区别则是遍历数据的方式,使用Collection接口需要用户去进行迭代,也就是在集合的外部这称为外部迭代。而Streams则使用内部迭代,它会代替使用者在流的内部进行迭代,还会把流值存在某个地方,只需要通过函数进行操控就可以了,Streams的内部迭代还可以自动选择一种适合计算机硬件的数据表示和并行实现。同时流只能够遍历一次。
public class Test04 {
    public static void main(String[] args) {
        List<String> title = Arrays.asList("Java8", "In", "Action");
        Stream<String> s = title.stream();
        s.forEach(System.out::println);
        s.forEach(System.out::println);
    }
}
抛出异常:stream has already been operated upon or closed
总结:
  • 流并不存储其元素,这些元素可能存储在底层的集合中,或是按需生成。
  • 流的操作不会修改其数据源,而是会生成一个新的流。
  • 流的操作是惰性执行的,在需要其结果时才会执行,所以需要终止操作来产生结果,这个操作会强制执行之前的惰性操作。在使用终止操作之后这个流就不能在使用了。
  • Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
  • 而与迭代器又不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程。

二、流操作

1、分类

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类:中间操作终端操作

2、中间操作与终端操作

1、中间操作:可以连接起来的流操作称为中间操作,如:filter、map和limit可以连成一条流水线。中间操作会返回另一个流,这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理(它们很懒)。这是因为中间操作一般都是可以合并起来,在终端操作时一次性全部处理。
2、终端操作:关闭流的操作称为终端操作。 终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer、甚至void。

3、使用流步骤

使用流三部曲:
  • 一个数据源(如集合)来执行一条查询。
  • 一个中间操作链,形成一条流的流水线。
  • 一个终端操作,执行流水线,并能生成结果。

(二)流(Stream)

上一篇:简洁又快速地处理集合——Java8 Stream(上)


下一篇:JDK1.8 Stream Collectors常用方法