• Java 8 Stream API 引入和使用


    Java 8 系列文章 持续更新中

    引入流

    流是什么

    流是Java API的新成员,它允许你以声明性的方式处理数据集合。可以看成遍历数据集的高级迭代。流可以透明地并行处理,无需编写多线程代码。我们先简单看一下使用流的好处。下面两段代码都是用来返回年龄小于14岁的初中生的姓名,并按照年龄排序。

    假如我们有下面Student实体类

    @Data
    public class Student {
        private String name;
        private int age;
        private boolean member;
        private Grade grade;
        public Student() {
        }
        public Student(String name, int age, boolean member, Grade grade) {
            this.name = name;
            this.age = age;
            this.member = member;
            this.grade = grade;
        }
        public enum Grade{
            JUNIOR_ONE,JUNIOR_TWO,JUNIOR_THREE
        }
    }
    

    Java 8之前的实现方式:

    List students = Arrays.asList(
            new Student("张初一", 13, false, Student.Grade.JUNIOR_ONE),
            new Student("李初二", 14, false, Student.Grade.JUNIOR_TWO),
            new Student("孙初三", 15, false, Student.Grade.JUNIOR_THREE),
            new Student("王初一", 12, false, Student.Grade.JUNIOR_ONE),
            new Student("钱初二", 14, false, Student.Grade.JUNIOR_TWO),
            new Student("周初三", 16, false, Student.Grade.JUNIOR_THREE));
    List resultStudent = new ArrayList<>(); //垃圾变量,一次性的中间变量
    //foreach循环,根据条件筛选元素
    for (Student student : students) {
        if (student.getAge() < 14) {
            resultStudent.add(student);
        }
    }
    //匿名类,给元素排序
    Collections.sort(resultStudent, new Comparator() {
        @Override
        public int compare(Student o1, Student o2) {
            return Integer.compare(o1.getAge(), o2.getAge());
        }
    });
    List resultName = new ArrayList<>();
    //foreach循环,获取元素属性
    for (Student student : resultStudent) {
        resultName.add(student.getName());
    }
    

    Java 8流的实现方式:

    List resultName = students.stream()
            .filter(student -> student.getAge() < 14) //年龄筛选
            .sorted(Comparator.comparing(Student::getAge)) //年龄排序
            .map(Student::getName) //提取姓名
            .collect(Collectors.toList());//将提取的姓名保存在List中
    

    为了利用多核架构并行执行这段代码,只需要把stream()替换成parallelStream()即可。

    通过对比两段代码之后,Java8流的方式有几个显而易见的好处。

    • 代码是以声明性的方式写的:说明想要做什么(筛选小于14岁的学生)而不是去说明怎么去做(循环、if)
    • 将几个基础操作链接起来,表达复杂的数据处理流水线(filter->sorted->map->collect),同时保持代码清晰可读。

    总结一下,Java 8的Stream API带来的好处:

    • 声明性-更简洁,更易读
    • 可复合-更灵活
    • 可并行-性能更好

    流简介

    流到底是什么?简单定义:“从支持数据处理操作的源生成的元素序列”,下面剖析这个定义。

    • 元素序列:像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合讲的是数据,流讲的是计算。
    • :流使用一个提供数据的源,如集合、数组或输入/输出资源。
    • 数据处理操作:流的数据处理功能之处类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流的操作可以顺序执行,也可以并行执行。
    • 流水线:很多流的操作会返回一个流,这样多个操作就可以链接起来,形成一个流水线。可以看成数据库式查询。
    • 内部迭代:于迭代器显示迭代的不同,流的迭代操作是在背后进行的。

    看一段代码,更好理解这些概念

    List resultName = students.stream() //从列表中获取流
            .filter(student -> student.getAge() < 16) //操作流水线:筛选
            .map(Student::getName) //操作流水线:提取姓名
            .limit(2) //操作流水线:只取2个
            .collect(Collectors.toList());//将结果保存在List中
    

    在上面代码中,数据源是学生列表(students),用来给流提供一个元素序列,调用stream()获取一个流,接下来就是一系列数据处理操作:filter、map、limit和collect。除collect之外,所有这些操作都会返回一个流,组成了一条流水线。最后collect操作开始处理流水线,并返回结果。

    流与集合

    粗略的说,流与集合之间的差异就在于什么时候进行计算。

    • 集合是一个内存中的数据结构(可以添加或者删除),它包含数据结构中目前所有的值——集合中的每个元素都是预先处理好然后添加到集合中的。
    • 流则是在概念上固定的数据结构(不能添加或删除元素),其元素是按需计算的。

    在哲学中,流被看作在时间中分布的一组值,而集合则是空间(计算机内存)中分布的一组值,在一个时间点上全体存在。

    只能遍历一次

    和迭代器类似,流只能遍历一次。遍历完成之后,我们说这个流已经被消费掉了。

    例如下面的代码会抛出异常

    Stream stream = students.stream();
    stream.forEach(System.out::println);
    stream.forEach(System.out::println);
    

    执行之后抛出如下异常:

    Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
    	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    	at com.example.demo.java8.stream.StreamTest.main(StreamTest.java:58)
    

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

    外部迭代与内部迭代

    我们使用iterator或者foreach遍历集合时的这种迭代方式被称为外部迭代,而Streams库使用内部迭代,它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。

    下面的代码说明了这种区别。

    • 外部迭代
    //使用增强for循环做外部迭代,底层还是迭代器
    List resultName = new ArrayList<>();
    for (Student student : students) {
        resultName.add(student.getName());
    }
    //使用迭代器做外部迭代
    Iterator iterator = students.iterator();
    while (iterator.hasNext()){
        Student student = iterator.next();
        resultName.add(student.getName());
    }
    
    • 内部迭代
    List resultName = students.stream()
            .map(Student::getName)
            .collect(Collectors.toList());
    

    流操作

    java.util.stream中的Stream接口定义了许多操作。可以分为两大类。先看一下下面这个例子:

    List resultName = students.stream() //从列表中获取流
            .filter(student -> student.getAge() < 16) //中间操作
            .map(Student::getName) //中间操作
            .limit(2) //中间操作
            .collect(Collectors.toList());//将Stream转为List
    

    可以看到两类操作:

    • filter、map和limit链接的一条流水线
    • collect触发流水线执行并关闭它
      流水线中流的操作称为中间操作,关闭流的操作称为终端操作

    中间操作

    诸如filter或sorted等中间操作会返回一个流,这让很多操作链接起来形成一个复合的流水线(查询)。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很(延迟计算/惰性求值)。
    因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
    修改一下上面的代码,看一下发生了什么:

    List resultName = students.stream() //从列表中获取流
            .filter(student -> {
                System.out.println("filter:"+student.getName());
                return student.getAge() < 16;
            }) //中间操作
            .map(student -> {
                System.out.println("map:"+student.getName());
                return student.getName();
            }) //中间操作
            .limit(3) //中间操作
            .collect(Collectors.toList());//将Stream转为List
    

    执行结果如下:

    filter:张初一
    map:张初一
    filter:李初二
    map:李初二
    filter:孙初三
    map:孙初三
    

    可以发现,利用流的延迟性质实现了几个好的优化。limit操作实现了只选择前3个,filter和map操作是相互独立的操作,但他们合并到同一次遍历中。

    终端操作

    终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如List、Integer,亦或是void等。

    流的使用

    流的使用一般包括三件事:

    • 一个数据源(如集合)来执行一个查询
    • 一个中间操作链,形成一条流水线
    • 一个终端操作,执行流水线,并生成结果

    流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链来设置一套配置(对流来说这就是一个中间操作链),接着时调用build方法(对流来说就是终端操作)。


    使用流

    Stream API支持许多操作,这些操作能帮助我们快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。

    筛选和切片

    用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。

    用谓词筛选 filter

    流支持filter方法,该方法接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如,下面的代码就是筛选是团员的学生:

    List memberList = students.stream()
            .filter(Student::isMember) //方法引用检查学生是否是团员
            .collect(Collectors.toList());
    

    去重元素 distinct

    流支持distinct方法,该方法可以将列表中的元素去重(根据流所生成元素的hashCode和equals方法实现)的流。例如下面的代码事筛选列表中的偶数,并去重:

    List numberList = Arrays.asList(1, 2, 3, 4, 5, 6, 2, 4, 6);
    numberList.stream()
            .filter(n -> n % 2 == 0)
            .distinct()
            .forEach(System.out::println);
    

    截短流 limit

    流支持limit(n)方法,该方法返回一个不超过给定长度的流。如果是有序的流,则最多返回前n个元素。例如,筛选出小于14岁的前三个学生:

    List studentList = students.stream()
            .filter(student -> student.getAge() < 14)
            .limit(3)
            .collect(Collectors.toList());
    

    跳过元素 skip

    流支持skip(n)方法,该方法返回一个跳过了前n个元素的流。如果流中的元素不足n个,则返回一个空流。例如,跳过年龄小于14岁的头三个学生,返回剩下的。

    List studentList = students.stream()
            .filter(student -> student.getAge() < 14)
            .skip(3)
            .collect(Collectors.toList());
    

    映射

    有时候当我们在处理数据时,需要从一系列对象中提取某个属性值,比如从SQL表中选择一列。Stream API通过map和flatMap方法提供了类似的工具。

    对流中的每一个元素应用函数 map

    流支持map方法,该方法接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。例如,需要提取学生列表中的学生姓名:

    List studentList = students.stream()
            .map(Student::getName)//getName会返回String,此时map返回的就是Stream
            .collect(Collectors.toList());
    

    如果需要进一步操作,例如获取姓名的长度,在链接上一个map即可:

    List studentList = students.stream()
            .map(Student::getName)
            .map(String::length)
            .collect(Collectors.toList());
    

    流的扁平化 flatMap

    先举个例子,我们需要从["hello", "world"]单词列表中提取每个字符并去重,结果应该是["h", "e", "l", "o", "w", "r", "d"]

    我们可能会写出下面这样的代码:

    List wordList = Arrays.asList("hello","word");
    List result = wordList.stream()
            .map(word -> word.split(""))
            .distinct()
            .collect(Collectors.toList());
    

    但实际上map返回的是Stream,或者我们又写出下面这样的代码:

    List wordList = Arrays.asList("hello","word");
    List> result = wordList.stream()
            .map(word -> word.split(""))
            .map(Arrays::stream)
            .distinct()
            .collect(Collectors.toList());
    

    但实际上map返回的是Stream>,而我们真想想要的是Stream

    这时候flatMap就派上用场了:

    List wordList = Arrays.asList("hello","word");
    List result = wordList.stream()
            .map(w -> w.split(""))
            .flatMap(Arrays::stream)
            .distinct()
            .collect(Collectors.toList());
    System.out.println(result);
    

    map(Arrays::stream)是将每个元素都映射成一个流,而flatMap方法效果将映射的流合并起来,即扁平化一个流。意思就是将流中的每个值都转换成一个流,然后把所有的流连接起来成为一个流

    查找和匹配

    还有在处理数据时,我们会判断一个集合中的某些元素是否符合给定的条件。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。

    检查谓词是否至少匹配一个元素 anyMatch

    anyMatch方法可以用来判断“流中是否有一个元素能匹配给定的谓词”。

    if(students.stream().anyMatch(Student::isMember)) {
        System.out.println("学生列表中至少有一个是团员!");
    }
    

    anyMatch方法返回一个boolean,是一个终端操作。

    检查谓词是否匹配所有元素 allMatch noneMatch

    allMatch方法可以用来判断“流中是否所有的元素都能匹配给定的谓词”

    if (students.stream().allMatch(student -> student.getAge() < 18)) {
        System.out.println("学生列表中所有学生的年龄都小于18岁!");
    }
    

    noneMatch和allMatch是相对的,用来判断“流中的所有元素都不能匹配给定的谓词”

    if (students.stream().noneMatch(student -> student.getAge() >= 18)) {
        System.out.println("学生列表中没有学生的年龄大于等于18岁!");
    }
    

    anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是我们熟悉的Java中的&&和||运算符短路在流中的版本。

    查找元素 findAny

    findAny方法返回当前流中的任意元素。可与其他流操作相结合使用。例如,我们需要找到一个学生列表中的团员:

    Optional studentOptional = students.stream()
            .filter(Student::isMember)
            .findAny();
    

    不过这个Optional是什么?

    Optional简介
    Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。上面代码中有可能什么元素都没有找到。Java 8引入Optional用来避免null带来的异常。

    先简单了解下它的几个方法:

    • isPresent():若Optional包含值则返回true,否则返回false。
    • ifPresent(Consumer consumer):若Optional包含值时执行给定的代码。参数是Consumer,一个函数式接口。
    • T get():若Optional包含值时返回该值,否则抛出NoSuchElementException异常。
    • T orElse(T other):若Optional包含值时返回该值,否则返回指定默认值。

    例如,前面的studentOptional若包含一个学生,我们就打印该学生的姓名,否则就什么也不做。

    studentOptional.ifPresent(student -> System.out.println(student.getName()));
    

    查找第一个元素 findFirst

    findFirst方法返回当前流中的第一个元素。在某些顺序流中,我们要找到第一个元素,这时可以使用findFirst,例如,给定一个数字列表,找到其中第一个平方根能被2整除的数:

    List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    Optional first = numbers.stream()
            .map(n -> n * n)
            .filter(n -> n % 2 == 0)
            .findFirst();
    first.ifPresent(n -> System.out.println(n));// 4
    

    我们会发现findAny和findFirst的工作方式是类似的,那我们什么时候使用findFirst和findAny呢?
    findFirst在并行上限制很多,所以如果不关心返回元素是哪一个(不关心顺序),请使用findAny,因为在并行时限制较少。

    归约 reduce

    reduce操作可以将一个流中的元素组合起来,得到一个值。

    元素求和

    使用foreach循环求和:

    List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    int sum = 0;
    for (Integer number : numbers) {
        sum += number;
    }
    

    这里通过反复加法,把一个列表归约成一个数字。如果是计算相乘的结果,是不是还要复制粘贴这段代码?大可不必,reduce操作将这种模式做了抽象。用reduce求和:

    Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
    

    这里reduce()接受两个参数:

    • 初始值,这里是0
    • BinaryOperator组合两个元素产生新值,这里是Lambda(a, b) -> a + b
      如果是相乘,则只需传递Lambda(a, b) -> a * b即可:
    Integer result = numbers.stream().reduce(0, (a, b) -> a * b);
    

    Java 8中,Integer类有了static int sum(int a, int b)方法来求两个数的和,这刚好是BinaryOperator类型的值,所以代码可以更精简,更易读:

    Integer sum = numbers.stream().reduce(0, Integer::sum);
    

    reduce还有一个重载版本,不接受初始值,返回一个Optional对象,考虑流中没有任何元素,也没有初始值,所以reduce无法返回结果,此时Optional中的值就不存在。

    最大值和最小值

    类比求和的操作,我们传递Lambda(a, b) -> a > b ? a : b即可:

    numbers.stream().reduce((a, b) -> a > b ? a : b);
    

    相同的Java 8中的Integer也新增了max和min来求两个数中的最大和最小值,则可以写成:

    Optional max = numbers.stream().reduce(Integer::max);
    Optional min = numbers.stream().reduce(Integer::min);
    

    数值流

    前面用reduce计算流中元素的中和,现在我想计算学生列表中学生的年龄总和,就可以这么写:

    students.stream().map(Student::getAge)
            .reduce(0, Integer::sum);
    

    但是这里暗含了装箱成本。Integer需要被拆箱成原始类型,在进行求和。所以Stream API提供了原始类型流特化,专门支持处理数流的方法。

    原始类型流

    Java 8中引入了三个原始类型流:IntStreamDoubleStreamLongStream,分别将流中的元素转化为int、long和double,从而避免暗含装箱成本。每个接口都带来了进行常用数值归约的新方法,如对数值流求和的sum,找到最大元素的max,还可以把它们再转回对象流。

    映射到数值流 mapToInt mapToDouble mapToLong

    students.stream()
            .mapToInt(Student::getAge)
            .sum();
    

    转回对象流 .boxed()

    Stream stream = students.stream()
            .mapToInt(Student::getAge)
            .boxed();
    

    默认值 OptionalInt OptionalDouble OptionalLong

    OptionalInt optionalInt = students.stream()
            .mapToInt(Student::getAge)
            .max();
    

    数值范围 range rangeClosed

    Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助我们生成一个数值范围。range和rangeClosed,两个方法接收两个参数第一个是起始值,第二个是结束值。前者不包含结束值,后者包含结束值。

    IntStream range = IntStream.range(1, 100);
    System.out.println(range.count());//99
    IntStream intStream = IntStream.rangeClosed(1, 100);
    System.out.println(intStream.count());//100
    

    数值流例子

    100以内的勾股数:a*a + b*b = c*c,a、b、c都是整数。

    IntStream.rangeClosed(1, 100)
            .boxed()
            .flatMap(a ->
                    IntStream.rangeClosed(a, 100)
                            .mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)})
                            .filter(ints -> ints[2] % 1 == 0))
            .forEach(t -> 
                    System.out.println((int) t[0] + "--" + (int) t[1] + "--" + (int) t[2]));
    

    构建流

    前面我们已经了解了很多流的的操作,并且知道通过stream方法从集合生成流以及根据数值范围创建数值流。下面我们我们介绍如何从序列、数组、文件和生成函数来创建流。

    由序列创建流 Stream.of

    使用静态方法Stream.of显示的创建一个流,该方法接受任意数量的参数。

    //创建一个字符串流
    Stream stringStream = Stream.of("A", "B", "C", "D", "E");
    //创建一个空流
    Stream.empty();
    

    由数组创建流 Arrays.stream

    使用静态方法Arrays.stream从数组创建一个流,该方法接受一个数组参数。

    int[] num = {1,2,3,4,5};
    IntStream stream = Arrays.stream(num);
    System.out.println(stream.sum());//15
    

    由文件生成流

    Java 8更新了NIO API(非阻塞 I/O),其中java.nio.file.Files中新增了许多静态方法可以返回一个流,如Files.lines()该方法接受一个文件路径对象(Path对象),返回由指定文件中每一行组成的字符串流。下面的代码用来计算这个文件中有多少个不同的字符:

    try (Stream lines = Files.lines(Paths.get("C:\\Users\\symon\\Desktop\\test.txt"), Charset.defaultCharset())) {
        long count = lines
                .flatMap(line -> {
                    System.out.println(line);
                    return Arrays.stream(line.split(""));
                })
                .distinct()
                .count();
        System.out.println(count);
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    由函数生成流,创建无限流

    Stream API提供了两个静态方法来生成流:Stream.iterate()Stream.generate()。这两个操作可以创建所谓的无限流:没有固定大小的流。一般来说,会使用limit(n)来进行限制,避免无尽地计算下去。

    迭代 Stream.iterate
    Stream.iterate(0, n -> n + 2)
            .limit(20)
            .forEach(System.out::println);
    

    iterate方法接受一个初始值(这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型)。这里是n -> n + 2,返回前一个元素加2。所以上面代码生成了一个正偶数流。如果不加limit限制,则会永远计算下去。

    生成 Stream.generate
    Stream.generate(Math::random)
            .limit(10)
            .forEach(System.out::println);
    

    generate方法接受一个Supplier类型的Lambda作为参数,不会像iterate一样对每个新生成的值应用函数。上面代码是取10个0~1之间的随机数。同样,如果不加limit限制,该流也会无限长。

  • 相关阅读:
    一言不合就重构
    已解决Python配置环境变量失效问题
    docker - window Docker Desktop升级
    字体图标 icon-font
    Spring Task
    一个超强的机器学习库
    使用jenkins连接linux部署jar包
    多版本并发控制MVCC
    weui citypicker 多次动态赋值
    Arcgis提取点数据经纬度
  • 原文地址:https://www.cnblogs.com/ssymon/p/16825465.html