zoukankan      html  css  js  c++  java
  • Java 8 Stream API的使用示例

    前言

    Java Stream API借助于Lambda表达式,为Collection操作提供了一个新的选择。如果使用得当,可以极大地提高编程效率和代码可读性。

    本文将介绍Stream API包含的方法,并通过示例详细展示其用法。


    一、Stream特点

    Stream不是集合元素,它不是数据结构也不保存数据,而更像一个高级版本的迭代器(Iterator)。Stream操作可以像链条一样排列,形成Stream Pipeline,即链式操作。

    Stream Pipeline由数据源的零或多个中间(Intermediate)操作和一个终端(Terminal)操作组成。中间操作都以某种方式进行流数据转换,将一个流转换为另一个流,转换后元素类型可能与输入流相同或不同,例如将元素按函数映射到其他类型或过滤掉不满足条件的元素。 终端操作对流执行最终计算,例如将其元素存储到集合中、遍历打印元素等。

    Stream特点:

    • 无存储。Stream不是一种数据结构,也不保存数据,数据源可以是一个数组,Java容器或I/O Channel等。

    • 为函数式编程而生。对Stream的任何修改都不会修改数据源,例如对Stream过滤操作不会删除被过滤的元素,而是产生一个不包含被过滤元素的新Stream。

    • 惰性执行。Stream上的中间操作并不会立即执行,只有等到用户真正需要结果时才会执行。

    • 一次消费。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

    注意:没有终端操作的流管道是静默无操作的,所以不要忘记包含一个终端操作。

    二、用法示例

    以下将基于《Java 8 Optional类使用的实践经验》一文中的Person类,展示Stream API的用法。考虑到代码简洁度,示例中尽量使用方法引用。

    2.1 Stream创建

    2.1.1 通过参数序列创建Stream

    对于可变参数序列,通过Stream.of()创建Stream,而不必先创建Array再创建Stream。

    IntStream stream = IntStream.of(10, 20, 30, 40, 50); // 不要使用Stream<Integer>
    Stream<String> colorStream = Stream.of("Red", "Pink", "Purple");
    Stream<Person> personStream = Stream.of(
            new Person("mike", "male", 10),
            new Person("lucy", "female", 4),
            new Person("jason", "male", 5)
    );
    

    2.1.2 通过数组创建Stream

    不用区分基础数据类型,但参数只能是数组。

    int[] intNumbers = {10, 20, 30, 40, 50};
    IntStream stream = IntStream.of(intNumbers);
    

    2.1.3 通过集合(Collection子类)创建Stream

    调用parallelStream()或stream().parallel()方法可创建并行Stream。

    Stream<Integer> numberStream = Arrays.asList(10, 20, 30, 40, 50).stream();
    

    2.1.4 通过生成器创建Stream

    · 通常用于随机数、元素满足固定规则的Stream,或用于生成海量测试数据的场景。

    · 应配合limit()、filter()使用,以控制Stream大小,否则stream长度无限。

    Stream.generate(Math::random).limit(10)
    Stream.generate(() -> (int) (System.nanoTime() % 100)).limit(5)
    

    2.1.5 通过iterate创建Stream

    · 重复对给定种子值(seed)调用指定的函数来创建Stream,其元素为seed, f(seed), f(f(seed))...无限循环。

    · 通常用于随机数、元素满足固定规则的Stream,或用于生成海量测试数据的场景。

    · 应配合limit()、filter()使用,以控制Stream大小,否则stream长度无限。

    // 按行依次输出:0、5、10、15、20
    Stream.iterate(0, n -> n + 5).limit(5).forEach(System.out::println);
    

    2.1.6 通过区间创建整数序列Stream

    用于IntStream、LongStream,range()不包含尾元素,rangeClosed()包含尾元素。

    LongStream longRange = LongStream.range(-100L, 100L); // 生成[-100, 100)区间的元素序列
    

    2.1.7 通过IO方式创建Stream

    · 适用于从文本文件中逐行读取数据、遍历文件目录等场景。

    · 通常配合try ... with resources语法使用,以安全而简洁地关闭资源。

    try (Stream<String> lines = Files.lines(Paths.get("./file.txt"), StandardCharsets.UTF_8)) {
                // 跳过第一行,输出第2~4共计三行
                lines.skip(1).limit(3).forEach(System.out::println);
            } catch (IOException e){
                System.out.println("Oops!");
            }
    

    2.2 Stream操作

    常见的操作可以归类如下:

    Intermediate:Stream经过此类操作后,结果仍为Stream

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

    Terminal:Stream里包含的内容按照某种算法汇聚为一个值

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

    基本的Stream用法格式为Stream.Intermediate.Terminal(SIT)Java8特性详解 lambda表达式 Stream以图示形式直观描述了这种格式及若干Intermediate操作。

    本节主要介绍常用操作及代码示例。为便于演示,首先定义如下集合对象:

    List<Person> persons = Arrays.asList(
            new Person("mike", "male", 10).setLocation("China", "Nanjing"),
            new Person("lucy", "female", 4),
            new Person("jason", "male", 5).setLocation("China", "Xian")
    );
    

    2.2.1 map + sum + filter + reduce

    只有IntStream、LongStream和DoubleStream支持sum()方法。

    // 计算年龄总和:totalAge = 19
    int totalAge = persons.stream().mapToInt(Person::getAge).sum();
    // 并行计算年龄总和,此处不建议使用reduce(针对复杂的规约操作)
    persons.stream().parallel().mapToInt(Person::getAge).reduce(0, Integer::sum);
    // 计算男生年龄总和:totalAge = 15
    persons.stream().filter(person -> "male".equals(person.getGender())).mapToInt(Person::getAge).sum();
    

    2.2.2 map + average + max

    average()返回OptionalDouble,max()/min()返回OptionalInt或Optional

    // 计算年龄均值,输出6.333333333333333
    persons.stream().mapToInt(Person::getAge).average().ifPresent(System.out::println);
    // 计算字典序最大的人名,输出mike
    persons.stream().map(Person::getName).max(String::compareToIgnoreCase).ifPresent(System.out::println);
    

    2.2.3 map + forEach

    // 输出每个学生姓名的大写形式,按行输出:MIKE、LUCY、JASON
    persons.stream()
            .map(Person::getName) // 将Person对象映射为String(姓名)
            .map(String::toUpperCase) // 将姓名转换大写
            .forEach(System.out::println); // 按行输出List元素
    

    2.2.4 collect

    · collect操作可将Stream元素转换为不同的数据类型,如字符串、List、Set和Map等。

    · Java 8通过Collectors类支持各种内置收集器,以简化collect操作。

    // 得到字符串:Colors: Red&Pink&Purple!
    colorStream.collect(Collectors.joining("&", "Colors: ", "!"));
    // 得到ArrayList,元素为:Red, Pink, Purple
    // 注意,Stream转换为数组的格式形如stream.toArray(String[]::new)
    colorStream.collect(Collectors.toList());
    // 得到HashSet,元素为:Red, Pink, Purple
    colorStream.collect(Collectors.toSet());
    // 得到LinkedList,toCollection()用于指定集合类型
    colorStream.collect(Collectors.toCollection(LinkedList::new));
    // 得到HashMap,{mike=Person{name='mike'}, jason=Person{name='jason'}, lucy=Person{name='lucy'}}
    personStream.collect(Collectors.toMap(Person::getName, Function.identity()));
    

    collect收集器还提供summingInt()、averagingInt()和summarizingInt()等计算方法。

    // 返回流中整数属性求和,即19
    persons.stream().collect(Collectors.summingInt(Person::getAge))
    // 计算流中Integer属性的平均值,即6.333333333333333
    persons.stream().collect(Collectors.averagingInt(Person::getAge))
    // 收集流中Integer属性的统计值,即IntSummaryStatistics{count=3, sum=19, min=4, average=6.333333, max=10}
    persons.stream().collect(Collectors.summarizingInt(Person::getAge))
    

    2.2.5 sorted + collect

    // 按照年龄升序排序:sortedpersons = [Person{name='lucy'}, Person{name='jason'}, Person{name='mike'}]
    List<Person> sortedPersons = persons.stream()
            .sorted(Comparator.comparingInt(Person::getAge)) // 按照年龄排序
            .collect(Collectors.toList()); // 汇聚为一个List对象
    // 按照姓名长度升序排序,按行输出:mike: 4、lucy: 4、jason: 5
    persons.stream()
            .sorted(Comparator.comparingInt(p -> p.getName().length()))
            .map(Person::getName)
            .map(name -> name + ": " + name.length())
            .forEach(System.out::println);
    

    2.2.6 map + anyMatch

    // 判断是否存在名为jason的人:existed = true
    boolean existed = persons.stream()
            .map(Person::getName)
            .anyMatch("jason"::equals); // 任意匹配项是否存在
    

    2.2.7 groupingBy + map + reduce

    // 将所有人按照性别分组并计数,输出:{female=1, male=2}
    Map<String, Long> groupBySex = persons.stream().collect(groupingBy(Person::getGender, counting()));
    System.out.println(groupBySex);
    // 将所有人按照性别分组并计算各组最大年龄,输出:Person{name='mike'}
    Map<String, Optional<Person>> groupBySexAge = persons.stream().collect(
            groupingBy(Person::getGender, maxBy(Comparator.comparingInt(Person::getAge))));
    System.out.println(groupBySexAge.get("male").get());
    // 将所有人按照性别分组,按行输出:female: lucy、male: mike,jason
    persons.stream().collect(groupingBy(Person::getGender))
            .forEach((k, v) ->System.out.println(k + ": "
                    + v.stream().map(Person::getName)
                    .reduce((x, y) -> x + "," + y).get()));
    

    注意,本例采用import static java.util.stream.Collectors.*;这种静态导入的方式简化Collectors.groupingBy()的调用,代码更简洁易读。此外,不推荐示例中forEach()的用法。

    2.2.8 maps + collect

    // 计算身高比例分布:agePercentages = [52.63%, 21.05%, 26.32%]
    List<String> agePercentages = persons.stream()
            .mapToInt(Person::getAge) // 将Person对象映射为年龄整型值
            .mapToDouble(age -> age / (double)totalAge * 100) // 计算年龄比例
            .mapToObj(new DecimalFormat("##.00")::format) // DoubleStream -> Stream<String>
            .map(percentage -> percentage + "%") // 添加百分比后缀
    
            .collect(Collectors.toList());
    // 若元素数目较多,可先定义formator = new DecimalFormat("##.00"),再调用mapToObj(formator::format)
    

    2.2.9 flatMap

    flatMap()将Stream中的集合实例内的元素全部拍平铺开,形成一个新的Stream,从而到达合并的效果。

    // 传统写法(注意两层循环)
    private static int countPrefix(List<List<String>> nested, String prefix) {
        int count = 0;
        for (List<String> element : nested) {
            if (element != null) {
                for (String str : element) {
                    if (str.startsWith(prefix)) {
                        count++;
                    }
                }
            }
        }
        return count;
    }
    // Stream写法
    private static int countPrefixWithStream(List<List<String>> nested, String prefix) {
        return (int) nested.stream()
                .filter(Objects::nonNull)
                .flatMap(Collection::stream)
                .filter(str -> str.startsWith(prefix))
                .count();
    }
    
    List<List<String>> lists = Arrays.asList(
            Arrays.asList("Jame"),
            Arrays.asList("Mike", "Jason"),
            Arrays.asList("Jean", "Lucy", "Beth")
    );
    System.out.println("以J开头的人名数:" + countPrefixWithStream(lists, "J"));
    

    三、规则总结

    使用Stream时,需注意以下规则:

    1. 避免重用Stream。

      Java 8 Stream一旦被Terminal操作消费,将不能够再使用,必须为待执行的每个Terminal操作创建新的Stream链。在实际开发时,将共用的Stream实例定义为成员变量时,尤其容易犯错。

      重用Stream将报告stream has already been operated upon or closed的异常。

      若需要多次调用,可利用Stream Supplier实例来创建已构建所有中间操作的新Stream。例如:

      Supplier<Stream<String>> streamSupplier =
              () -> Stream.of("d2", "a2", "b1", "b3", "c")
                      .filter(s -> s.startsWith("a"));
      streamSupplier.get().anyMatch(s -> true);   // 每次调用get()构造一个新stream
      streamSupplier.get().noneMatch(s -> true);
      

      注意,anyMatch()方法接受Predicate引元,通常无需使用filter,此处仅为示例方便。

    2. 避免创建无限流。

      通过iterate或生成器创建Stream时,应配合limit()使用,以控制Stream大小。

      distinct()limit()共用时,应特别注意去重后元素数目是否满足limit限制。例如:

      IntStream.iterate(0, i -> (i + 1) % 2) // 生成0和1的整数序列   
          .distinct() // 去重后为0和1两个元素   
          .limit(10) // limit(10)限制得不到满足,从而变成无限流   
          .forEach(System.out::println);
      
    3. 注意Stream操作顺序,尽可能提前通过filter()等操作降低数据规模

      以下面一段简单的代码为例:

      Stream.of("a1", "b2", "c3", "d4", "e5").map(s -> {   
          System.out.println("map: " + s);
          return s.toUpperCase();
      }).filter(s -> {
          System.out.println("filter: " + s);
          return s.startsWith("A");
      }).forEach(s -> System.out.println("forEach: " + s));
      

      运行输出如下:

      map: a1
      filter: A1
      forEach: A1
      map: b2
      filter: B2
      map: c3
      filter: C3
      map: d4
      filter: D4
      map: e5
      filter: E5
      

      可见,流中的每个字符串都被调用5次map()filter(),而forEach()只调用一次。

      再改变操作顺序,将filter()移到Stream操作链的头部:

      Stream.of("a1", "b2", "c3", "d4", "e5").filter(s -> {
          System.out.println("filter: " + s);
          return s.startsWith("a");
      }).map(s -> {
          System.out.println("map: " + s);
          return s.toUpperCase();
      }).forEach(s -> System.out.println("forEach: " + s));
      

      运行输出如下:

      filter: a1
      map: a1
      forEach: A1
      filter: b2
      filter: c3
      filter: d4
      filter: e5
      

      可见,map()只被调用一次。虽然Stream惰性计算的特性使得操作顺序并不影响最终结果,但合理地安排顺序可以减少实际执行次数。数据规模较大时,性能会有较明显的提升。

    4. 注意Stream操作的副作用。

      大多数Stream操作必须是无干扰、无状态的。

      “无干扰”是指在流操作的过程中,不去修改流的底层数据源。例如,遍历流时不能通过添加或删除集合中的元素来修改集合。

      “无状态”是指Lambda表达式的结果不能依赖于流管道执行过程中,可能发生变化的外部作用域的任何可变变量或状态。

      以下代码试图在操作流时添加和移出元素,运行时均会抛出java.util.ConcurrentModificationException异常:

      List<String> strings = new ArrayList<>(Arrays.asList("one", "two"));
      String concatenatedString = strings.stream()
              // 不要这样做,干扰发生在这里
              .peek(s -> strings.add("three"))
              .reduce((a, b) -> a + " " + b)
              .get();
      List<Integer> list = IntStream.range(0, 10)
              .boxed() // 流元素装箱为Integer类型
              .collect(Collectors.toCollection(ArrayList::new));
      list.stream()
              .peek(list::remove) // 不要这样做,干扰发生在这里
              .forEach(System.out::println);
      

      以下代码对并行Stream使用了有状态的Lambda表达式:

      Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
      List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
      List<Integer> parallelStorage = new ArrayList<>();
      //List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
      listOfIntegers.parallelStream()
              // 不要这样做,此处使用了有状态的Lambda表达式
              .map(e -> { parallelStorage.add(e); return e; })
              .forEachOrdered(e -> System.out.print(e + " "));
      System.out.println(": 1st");
      parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));
      System.out.println(": 2nd");
      

      运行结果可能出现以下几种:

      // 并行执行流时,map()添加元素的顺序和随后的forEachOrdered()元素打印顺序不同
      1 2 3 4 5 6 7 8 : 1st
      1 6 3 2 7 8 5 4 : 2nd
      // 多线程可能同时读取到相同的下标n进行赋值,导致元素数量少于预期(采用synchronizedList可解决该问题)
      1 2 3 4 5 6 7 8 : 1st
      1 5 8 3 6 : 2nd
      

      《Effective Java 第三版》中指出,不要尝试并行化流管道,除非有充分的理由相信它将保持计算的正确性并提高其速度。 不恰当地并行化流的代价可能是程序失败或性能灾难。

    5. 避免过度使用Stream,否则可能使代码难以阅读和维护。

      常见的问题是Lambda表达式过长,可通过抽取方法等手段,尽量将Lambda表达式限制在几行之内。

  • 相关阅读:
    JavaScript对原始数据类型的拆装箱操作
    Javascript继承(原始写法,非es6 class)
    动态作用域与词法作用域
    自行车的保养
    探索JS引擎工作原理 (转)
    C语言提高 (7) 第七天 回调函数 预处理函数DEBUG 动态链接库
    C语言提高 (6) 第六天 文件(续) 链表的操作
    C语言提高 (5) 第五天 结构体,结构体对齐 文件
    C语言提高 (4) 第四天 数组与数组作为参数时的数组指针
    C语言提高 (3) 第三天 二级指针的三种模型 栈上指针数组、栈上二维数组、堆上开辟空间
  • 原文地址:https://www.cnblogs.com/clover-toeic/p/10940315.html
Copyright © 2011-2022 走看看