Java8实战笔记(二)

107 阅读7分钟

一、流

允许以声明式方式处理数据集合,说明想要完成什么,而不是说明如何实现一个操作。它与集合的区别在于:

  • 集合时一个内存中的数据结构,它包含数据结构中目前所有的值,集合中的每个元素都得先计算出来才能添加到集合中(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素 都得先算出来才能成为集合的一部分)。
  • 相比之下,流不能添加或删除原色,它时在概念上固定的数据机构,其元素时按需计算的,也就是说,用户仅仅从流中提取需要的值。另一种角度说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值。

二、只能遍历一次

流和迭代器类似,流只能遍历一次。遍历完之后,这个流已经被消费掉了,可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样。例如,以下代码会抛出一个异常,说流已经被消费掉了:

// 打印标题中的每个单词 
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out :: println);
// java.lang.IllegalStateException: stream has already been operated upon or closed
s.forEach(System.out::println); 

三、使用流

  1. 筛选和切片

    • filter():用谓词筛选,该操作接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

      // 筛选出所有素菜
      List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
      
    • distinct():筛选各异的元素,该操作返回一个元素各异(根据流所生成元素的hashCode和equal方法实现)的流。

      // 筛选出列表中所有的偶数,并确保没有重复
      List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
      numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out :: println);
      
    • limit(n):截断流,该方法返回一个不超过给定长度的流。所需的长度作为参数传递 给limit。如果流是有序的,limit(n)方法会保证返回的新流中元素的顺序与原流中相同,并且最多包括原流的前 n 个元素,如果n大于列表的长度,limit(n)操作不会抛出异常,而是返回一个包含原始列表中所有元素的流。

      // 筛选出热量超过300卡路里的头三道菜
      List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(Collectors.toList());
      
    • skip(n):跳过元素,该方法返回一个扔掉了前n个元素的流,如果流中元素不足n个,则返回一 个空流。

      // 跳过超过300卡路里的头两道菜,并返回剩下的
      List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(Collectors.toList());
      
  2. 映射

    • map():该方法接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射为一个新的元素。

      // 1、把方法引用Dish::getName传给了map方法,来提取流中菜肴的名称
      List<String> dishNames = menu.stream().map(Dish :: getName).collect(toList());
      // 2、给定一个单词列表,返回一个列表,显示每个单词有几个字母
      List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
      List<Integer> wordLengths = words.stream().map(String :: length).collect(toList());
      // 3、找出每道菜的名称有多长
      List<Integer> dishNameLengths = menu.stream().map(Dish :: getName).map(String :: length).collect(Collectors.toList());
      
    • flatMap():该方法把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

      // 对于一张词表,返回一张列表,列出里面各不相同的字符
      List<String> uniqueCharacters = words.stream() .map(w -> w.split("")).flatMap(Arrays::stream).distinct().collect(Collectors.toList()); 
      
  3. 查找和匹配

    • anyMatch():判断流中是否有一个元素能匹配给定的谓词。

      // 判断菜单里是否有素食可选择
      if(menu.stream().anyMatch(Dish :: isVegetarian)) {
          System.out.println("The menu is (somewhat) vegetarian friendly!!");
      }
      
    • allMatch():判断流中的元素是否都能匹配给定的谓词。

      // 判断菜品是否有利健康(即所有菜的热量都低于1000卡路里)
      boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
      
    • noneMatch() :确保流中没有任何元素与给定的谓词匹配

      // 判断菜品是否有利健康(即所有菜的热量都低于1000卡路里)
      boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
      
    • findAny():返回当前流中的任意元素

      // 筛选出一道素食菜肴
      Optional<Dish> dish = menu.stream().filter(Dish :: isVegetarian).findAny();
      
    • findFirst():返回当前流中的第一个元素

      // 给定一个数字列表,找出第一个平方能被3整除的数
      List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5); 
      Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst();
      
  4. 归约

    • reduce():用于将流中的元素累积成一个单一的结果。reduce 操作可以用于求和、求积、查找最大值和最小值等场景。

      // 求和,有初始值
      int sum = numbers.stream().reduce(0, Integer::sum);
      // 求和,无初始值
      Optional<Integer> sum = numbers.stream().reduce(Integer::sum);
      // 求最大值
      Optional<Integer> max = numbers.stream().reduce(Integer::max);
      // 求最小值
      Optional<Integer> max = numbers.stream().reduce(Integer::min);
      
    • count():计算流中元素的个数

      long count = menu.stream().count(); 
      
  5. 数值流

    • 映射到数值流:mapToInt()、mapToDouble()和mapToLong(),这几种方法跟map方法的工作方式一样,只是他们返回的是一个特化流(IntStream、DoubleStream和LongStream),而不是Stream。

      int calories = menu.stream()  // 返回一个Stream<Dish>
          .mapToInt(Dish :: getCalories) // 返回一个IntStream
          .sum()
      
    • 转换回对象流:boxed()

      // 将Stream转换为数值流
      IntStream intStream = menu.stream().mapToInt(Dish::getCalories); 
      // 将数值流转换为Stream
      Stream<Integer> stream = intStream.boxed(); 
      
    • 默认值OptionalInt:如果没有初始值,在求和或者其他操作时可以用Optional原始类 型特化版本:OptionalInt、OptionalDouble和OptionalLong。

      // 找到IntStream中的最大元素
      OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max(); 
      //如果没有最大值的话,显式提供一个默认最大值
      int max = maxCalories.orElse(1); 
      
    • 数值范围:IntStream和LongStream的静态方法range()和rangeClosed(),可以生成范围内的数值。

      // 生成一个一个从1到100的偶数流并输出流内元素个数
      IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
      System.out.println(evenNumbers.count());
      
  6. 构建流

    • 由值创建流:Stream.of()可以显式值创建一个流,它可以接受任意数量的参数。

      Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
      stream.map(String :: toUpperCase).forEach(System.out :: println);
      
    • 由数组创建流:Arrays.stream()从数组创建一个流,接受一个数组作为参数。

      int[] numbers = {2, 3, 5, 7, 11, 13};
      int sum = Arrays.stream(numbers).sum();
      
    • 由文件生成流:java.nio.file.Files中的很多静态方法都会返回一个流,例如Files.lines(),它会返回一个由指定文件中的各行构成的字符串流。

      // 计算一个文件中有多少各不相同的词
      long uniqueWords = 0;
      try{
          Stream<String> lines = Files.lines(Paths.get(data.txt), Charset.defaultCharset());
          uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
      } catch(IOException e) {
          e.printStackTrace();
      }
      
    • 由函数生成流:Stream.iterate()和Stream.generate()可以创建所谓的无限流,它们不像从固定集合创建的流那样由固定大小的流。由iterate 和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说, 应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

      // iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>类型)。这里,我们使用Lambda n -> n + 2,返回的是前一个元素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的,因为结果取决于前一次应用。用limit方法来显式限制流的大小。
      Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out :: println);
      // generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
      Stream.generate(Math::random).limit(5).forEach(System.out::println);
      

      总结:

      ① iterate():基于前一个元素生成下一个元素,适合生成有依赖关系的序列;接受一个UnaryOperator,即一个接受一个参数并返回一个结果的函数;适用于生成递增序列、斐波那契数列等有依赖关系的序列。

      ② generate():每个元素独立生成,适合生成没有依赖关系的序列;接受一个 Supplier,即一个不接受参数但返回一个结果的函数;适用于生成随机数、固定值等没有依赖关系的序列。