Stream API 进阶:筛选、映射、查找、归约

文章目录

      • 1. 引言 (Introduction)
      • 2. 筛选和切片 (Filtering and Slicing)
        • 2.1 使用谓词筛选 `filter`
        • 2.2 筛选各异的元素 `distinct`
        • 2.3 截短流 `limit`
        • 2.4 跳过元素 `skip`
      • 3. 映射 (Mapping)
        • 3.1 对流中每一个元素应用函数 `map`
        • 3.2 流的扁平化 `flatMap`
      • 4. 查找和匹配 (Finding and Matching)
        • 4.1 `anyMatch`:至少一个匹配
        • 4.2 `allMatch`:全部匹配
        • 4.3 `noneMatch`:全部不匹配
        • 4.4 `findAny`:任意一个
        • 4.5 `findFirst`:第一个
      • 5. 归约 (Reduction)
        • 5.1 `reduce`:元素求和
        • 5.2 `reduce`:最大值和最小值
      • 6. 数值流 (Numeric Streams)
        • 6.1 原始类型流特化
        • 6.2 数值范围
        • 6.3 数值流应用:勾股数
      • 7. 构建流 (Building Streams)
        • 7.1 由值创建流
        • 7.2 由数组创建流
        • 7.3 由文件生成流
        • 7.4 由函数生成流:创建无限流
          • 7.4.1 `Stream.iterate`
          • 7.4.2 `Stream.generate`
      • 8. 总结 (Conclusion)

1. 引言 (Introduction)

大家好!在上一篇“深入理解流(Streams)—— 声明式数据处理的艺术”中,我们深入探讨了 Java 8 流(Streams)的基础概念、特性、与集合的区别以及基本操作。我们了解到,流是一种声明式的、类似于 SQL 查询的数据处理方式,它允许我们以更简洁、高效、易读的代码操作数据。流的出现,让我们告别了繁琐的迭代和条件判断,专注于“做什么”而不是“怎么做”。

本篇我们将继续深入“流”的世界,聚焦于《Java 8 in Action》第五章的内容,详细讲解流的各种实用操作:筛选切片映射查找匹配归约。掌握这些高级操作将使你能够更加游刃有余地处理各种数据处理任务。通过丰富的代码示例,你将掌握如何利用这些操作高效处理数据,并体会到 Stream API 的强大之处。

本章内容概览:

  • 筛选和切片: 如何精准地选择和提取流中符合条件的元素。
  • 映射: 如何将流中的元素转换为另一种形式。
  • 查找和匹配: 如何快速验证流中是否存在满足条件的元素。
  • 归约: 如何将流中的元素组合成一个结果。
  • 数值流: 如何高效处理数值数据。
  • 构建流: 如何从不同的来源创建流。

让我们一起“玩转”流,让数据处理变得更加轻松有趣!

2. 筛选和切片 (Filtering and Slicing)

筛选和切片是流操作中最基础、最常用的部分。它们用于选择和提取流中符合特定条件的元素,就像 SQL 中的 WHERE 子句一样。

2.1 使用谓词筛选 filter

filter 操作接收一个 谓词(Predicate,一个返回 boolean 的函数)作为参数,并返回一个包含所有匹配谓词的元素的新流。 简单来说,filter 就像一个过滤器,只有满足条件的元素才能通过。

代码示例: 筛选出所有素食菜肴

List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH)
);List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian) // 使用方法引用,筛选素食菜肴.collect(Collectors.toList());System.out.println(vegetarianDishes);
//[Dish{name='french fries', vegetarian=true, calories=530, type=OTHER}, Dish{name='rice', vegetarian=true, calories=350, type=OTHER}, Dish{name='season fruit', vegetarian=true, calories=120, type=OTHER}, Dish{name='pizza', vegetarian=true, calories=550, type=OTHER}]

这里,Dish::isVegetarian 是一个方法引用,指向 Dish 类的 isVegetarian 方法。它就是一个谓词,用于判断一道菜是否为素食。

代码示例: 筛选出热量大于 300 的菜肴

List<Dish> highCaloricDishes = menu.stream().filter(dish -> dish.getCalories() > 300) // 使用 Lambda 表达式.collect(Collectors.toList());

与传统 for 循环对比:

// 使用for-each循环和if条件
List<Dish>  highCaloricDishes2 = new ArrayList<>();
for(Dish dish: menu){if(dish.getCalories() > 300){highCaloricDishes2.add(dish);}
}

Stream的优势,简洁明了。

2.2 筛选各异的元素 distinct

distinct 操作返回一个新流,其中包含原始流中所有不重复的元素(根据 equalshashCode 方法判断)。distinct 就像一个去重器,确保流中的元素都是独一无二的。

代码示例: 从数字列表中找出唯一的偶数

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println); // 输出 2 和 4

注意事项: 对于自定义对象,需要正确实现 equalshashCode 方法,才能确保 distinct 操作的正确性。

2.3 截短流 limit

limit(n) 操作返回一个新流,包含原始流中的前 n 个元素。limit 就像一把剪刀,可以截取流的前面一部分。

代码示例: 获取卡路里最高的前 3 道菜

List<Dish> highCaloricDishes = menu.stream().sorted(Comparator.comparing(Dish::getCalories).reversed()).limit(3).collect(Collectors.toList());

这里,先对菜单按照calories倒序,然后通过limit(3) 截取前三个元素。

2.4 跳过元素 skip

skip(n) 操作返回一个新流,其中跳过了原始流中的前 n 个元素。如果流中元素不足 n 个,则返回一个空流。 skip 像是跳过指定数量的元素。

代码示例: 跳过卡路里最高的前 2 道菜,获取剩下的菜肴

List<Dish> dishes = menu.stream().sorted(Comparator.comparing(Dish::getCalories).reversed()).skip(2).collect(Collectors.toList());

limit(n)skip(n) 可以组合使用,实现分页效果。

3. 映射 (Mapping)

映射操作用于将流中的元素转换为另一种形式,就像一个“变形器”,可以将元素从一种形态转变为另一种形态。

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

map 操作接收一个 函数 作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。map 可以将流中的元素从一种类型转换为另一种类型。

代码示例: 提取每道菜的名称

List<String> dishNames = menu.stream().map(Dish::getName) // 使用方法引用,提取菜名.collect(Collectors.toList());

这里,Dish::getName 是一个方法引用,它将 Dish 对象映射为它的名称(String 类型)。

代码示例: 将单词列表转换为单词长度列表

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream().map(String::length).collect(Collectors.toList());

这里通过map(String::length)将每个String 映射成 Integer,也就是它的长度。

与传统方式对比:

//传统方式
List<String>  dishNames2 = new ArrayList<>();
for(Dish dish : menu){dishNames2.add(dish.getName());
}

Stream的优势: 代码更简洁,意图更清晰。

3.2 流的扁平化 flatMap

flatMap 操作接收一个函数作为参数,这个函数会将每个元素映射成一个流,然后flatMap 会将这些流合并成一个流。flatMap 擅长处理嵌套的流结构,可以将多个流“拍扁”成一个流。

用图示说明 mapflatMap 的区别:

假设我们有一个包含多个列表的流: Stream<List<String>>

  • map: 如果使用 map,你会得到一个 Stream<Stream<String>>,也就是说,每个列表都被映射成了一个独立的流。
  • flatMap: 如果使用 flatMap,你会得到一个 Stream<String>,所有的列表都被“扁平化”成了一个包含所有单词的流。

代码示例: 从多个句子列表中提取出所有不重复的单词

List<String> sentences = Arrays.asList("Hello world", "Goodbye world", "Java 8 streams");List<String> uniqueWords = sentences.stream().map(sentence -> sentence.split(" ")) // 将每个句子分割成单词数组.flatMap(Arrays::stream) // 将每个单词数组转换为流,然后扁平化.distinct().collect(Collectors.toList());System.out.println(uniqueWords);
//[Hello, world, Goodbye, Java, 8, streams]
  1. map(sentence -> sentence.split(" ")): 将每个句子分割成单词数组。此时,流的类型是 Stream<String[]>
  2. flatMap(Arrays::stream): Arrays::stream 将每个 String[] 数组转换为一个 Stream<String>flatMap 将这些流合并成一个 Stream<String>
  3. distinct(): 去除重复的单词。
  4. collect(Collectors.toList()): 将结果收集到一个列表中。

代码示例: 给定两个数字列表,返回所有的数对

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);List<int[]> pairs = numbers1.stream().flatMap(i -> numbers2.stream().map(j -> new int[]{i, j})).collect(Collectors.toList());pairs.forEach(pair -> System.out.println("(" + pair[0] + ", " + pair[1] + ")"));
//输出:
//(1, 3)
//(1, 4)
//(2, 3)
//(2, 4)
//(3, 3)
//(3, 4)
  1. 内层map, 生成数对。
  2. 外层flatMap, 将 Stream<Stream<int[]>> 扁平化为 Stream<int[]>

4. 查找和匹配 (Finding and Matching)

在数据处理中,我们经常需要检查流中是否存在满足特定条件的元素,或者验证流是否符合某种模式。Java 8 流 API 提供了以下强大的工具来完成这些任务:

  • anyMatch:检查流中是否至少有一个元素匹配给定的谓词。
  • allMatch:检查流中是否所有元素都匹配给定的谓词。
  • noneMatch:检查流中是否没有元素匹配给定的谓词。
  • findAny:返回流中的任意一个元素(通常用于并行流)。
  • findFirst:返回流中的第一个元素

这些方法都接收一个 Predicate(谓词,一个返回布尔值的函数)作为参数,用于定义匹配条件。它们通常与 filter 操作结合使用,但也可以直接应用于原始流。

4.1 anyMatch:至少一个匹配

anyMatch 方法用于判断流中是否至少存在一个元素满足给定的条件。如果找到任何一个匹配的元素,它将立即返回 true,不会继续处理剩余的元素(短路特性)。

代码示例: 检查菜单中是否有素食选项(Dish::isVegetarian 是一个方法引用,指向 Dish 类的 isVegetarian 方法)

boolean hasVegetarianOption = menu.stream().anyMatch(Dish::isVegetarian);if (hasVegetarianOption) {System.out.println("The menu has at least one vegetarian dish.");
}

代码示例(变体): 使用 Lambda 表达式检查是否有热量超过 500 的菜肴

boolean hasHighCalorieDish = menu.stream().anyMatch(dish -> dish.getCalories() > 500);

短路特性: 一旦 anyMatch 找到一个匹配的元素,它就会立即返回 true,不会继续检查流中的剩余元素。这在处理大型流时可以显著提高效率。

4.2 allMatch:全部匹配

allMatch 方法用于判断流中的所有元素是否都满足给定的条件。如果所有元素都匹配谓词,它将返回 true;否则,只要遇到一个不匹配的元素,它就会立即返回 false(短路特性)。

代码示例: 检查菜单中是否所有菜肴的热量都低于 1000 卡路里

boolean allHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000);if (allHealthy) {System.out.println("All dishes are under 1000 calories.");
}

代码示例(变体): 检查一个字符串列表中是否所有字符串都以大写字母开头

List<String> words = Arrays.asList("Apple", "Banana", "Orange");
boolean allStartWithUppercase = words.stream().allMatch(word -> Character.isUpperCase(word.charAt(0)));

短路特性: allMatch 具有短路特性。一旦遇到一个不匹配的元素,它就会立即返回 false

4.3 noneMatch:全部不匹配

noneMatch 方法用于判断流中是否没有任何元素满足给定的条件。如果没有元素匹配谓词,它将返回 true;否则,只要遇到一个匹配的元素,它就会立即返回 false(短路特性)。

代码示例: 检查菜单中是否没有热量高于 1000 卡路里的菜肴

boolean noHighCalorieDishes = menu.stream().noneMatch(dish -> dish.getCalories() > 1000);if (noHighCalorieDishes) {System.out.println("There are no dishes over 1000 calories.");
}

代码示例(变体): 检查数字列表中, 是否不存在负数.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean noNegativeNumbers = numbers.stream().noneMatch(n -> n < 0);

短路特性: noneMatch 同样具有短路特性。

4.4 findAny:任意一个

findAny 方法返回流中的任意一个元素(通常是第一个,但在并行流中不保证顺序)。它返回一个 Optional 对象,用于处理可能不存在元素的情况(例如,流为空或没有元素匹配 filter 条件)。

代码示例: 找到任意一个素食菜肴

Optional<Dish> anyVegetarianDish = menu.stream().filter(Dish::isVegetarian).findAny();anyVegetarianDish.ifPresent(dish -> System.out.println("Found a vegetarian dish: " + dish.getName()));

findFirst 的区别:

  • 并行流: 在并行流中,findAny 通常比 findFirst 更高效。因为 findAny 不需要维护元素的顺序,它可以选择任何可用的元素,而 findFirst 必须等待第一个元素可用。
  • 顺序流: 在顺序流中,findAnyfindFirst 通常返回相同的结果(第一个元素)。但是,findAny 的语义更宽松,它不保证返回第一个元素。

代码示例(并行流):

Optional<Dish> anyVegetarianDishParallel = menu.parallelStream() // 使用并行流.filter(Dish::isVegetarian).findAny();
4.5 findFirst:第一个

findFirst 方法返回流中的第一个元素(如果存在)。它也返回一个 Optional 对象,以处理流为空的情况。

代码示例: 找到第一个素食菜肴

Java

Optional<Dish> firstVegetarianDish = menu.stream().filter(Dish::isVegetarian).findFirst();firstVegetarianDish.ifPresent(dish -> System.out.println("Found the first vegetarian dish: " + dish.getName()));

Optional 类型详解:

findAnyfindFirst 都返回 Optional 对象。Optional 是 Java 8 引入的一个容器类,用于表示一个值可能存在也可能不存在。它可以帮助我们更优雅地处理可能出现的空值,避免 NullPointerException

Optional 的主要方法:

  • isPresent(): 检查值是否存在。如果存在,返回 true;否则,返回 false
  • ifPresent(Consumer<? super T> consumer): 如果值存在,则使用该值调用给定的 Consumer(消费型函数)。
  • get(): 获取值。如果值存在,则返回该值;如果值不存在,则抛出 NoSuchElementException
  • orElse(T other): 如果值存在,则返回该值;否则,返回给定的默认值 other
  • orElseGet(Supplier<? extends T> other): 如果值存在,则返回该值;否则,调用给定的 Supplier(供给型函数)获取一个默认值。
  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果值存在,则返回该值;否则,抛出由给定的 Supplier 创建的异常。

使用 Optional 的最佳实践:

  • 避免直接调用 get() 除非你确定 Optional 对象中一定包含值,否则不要直接调用 get() 方法。因为它可能抛出 NoSuchElementException
  • 优先使用 ifPresent()orElse()orElseGet()orElseThrow() 这些方法提供了更安全、更优雅的方式来处理可能为空的值。
  • 链式调用: Optional 的方法通常可以链式调用,使代码更简洁。

代码示例(Optional 的链式调用):

String dishName = menu.stream().filter(Dish::isVegetarian).findFirst().map(Dish::getName) // 如果找到素食菜肴,则获取其名称.orElse("No vegetarian dish found"); // 如果没有找到,则返回默认值System.out.println(dishName);

这个例子展示了如何使用 Optional 来安全地获取值,并提供一个默认值以防没有找到匹配的元素。

5. 归约 (Reduction)

归约操作,顾名思义,就是将流中的元素 逐步结合 起来,最终 产生一个单一的结果。这个结果可以是数值(如总和、最大值、最小值),也可以是集合或其他自定义类型。你可以将归约操作想象成将一张长长的纸条不断对折,直到折叠成一个小方块。

在 Java 8 流 API 中,reduce 方法是执行归约操作的核心。它提供了强大的灵活性,可以处理各种各样的归约场景。

5.1 reduce:元素求和

reduce 方法最常见的用法之一就是对流中的数值元素进行求和。它有两种重载形式:

  1. reduce(T identity, BinaryOperator<T> accumulator):

    • identity: 初始值(累加器的起点)。
    • accumulator: 一个 BinaryOperator<T>(二元操作符),它接收两个参数(累加器的当前值和流中的下一个元素),并返回一个新的累加值。
  2. reduce(BinaryOperator<T> accumulator):

    • accumulator: 一个 BinaryOperator<T>(二元操作符),它接收两个相同类型的参数,并返回一个相同类型的结果。此重载形式不接受初始值。

代码示例(有初始值): 计算菜肴列表中所有菜肴的总热量

int totalCalories = menu.stream().map(Dish::getCalories) // Stream<Integer>.reduce(0, (a, b) -> a + b); // 初始值为 0,累加器是 (a, b) -> a + bSystem.out.println("Total calories: " + totalCalories);
  • map(Dish::getCalories): 将 Stream<Dish> 转换为 Stream<Integer>,提取出每道菜的热量。
  • reduce(0, (a, b) -> a + b):
    • 初始值:0
    • 累加器:(a, b) -> a + b,这是一个 Lambda 表达式,表示将当前累加值 a 与下一个元素 b 相加。
    • reduce 方法从初始值 0 开始,将 0 与第一个元素相加,得到一个新的累加值;然后,将新的累加值与第二个元素相加,以此类推,直到处理完所有元素。

代码示例(使用方法引用):

int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); // 使用 Integer.sum 方法引用System.out.println("Total calories: " + totalCalories);

Integer::sum 是一个方法引用,它等价于 Lambda 表达式 (a, b) -> a + b

代码示例(无初始值):

Optional<Integer> totalCalories = menu.stream().map(Dish::getCalories).reduce((a, b) -> a + b); // 没有初始值totalCalories.ifPresent(sum -> System.out.println("Total calories: " + sum));
  • 没有初始值时,reduce 方法返回一个 Optional<Integer> 对象。这是因为,如果流为空,那么就没有初始值,也就无法进行累加,所以结果可能不存在。
  • ifPresent 方法用于安全地处理 Optional 对象。如果 Optional 对象包含值(即流不为空),则执行给定的操作(打印总热量)。

与传统 for 循环对比:

// 使用 for 循环
int sum = 0;
for (Dish dish : menu) {sum += dish.getCalories();
}
System.out.println("Total calories (for loop): " + sum);

Stream API 的 reduce 方法使代码更简洁、更具声明性。我们只需要声明“做什么”(求和),而不需要关心“怎么做”(循环、累加)。

5.2 reduce:最大值和最小值

除了求和,reduce 方法还可以用于计算流中的最大值和最小值。

代码示例: 找出热量最高的菜肴

Optional<Dish> maxCalorieDish = menu.stream().reduce((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2);maxCalorieDish.ifPresent(dish -> System.out.println("Dish with maximum calories: " + dish.getName()));
  • 这里没有提供初始值,所以 reduce 方法返回一个 Optional<Dish> 对象。
  • 累加器:(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2。这是一个 Lambda 表达式,它比较两道菜的热量,返回热量较高的那道菜。

代码示例(使用方法引用):

Optional<Dish> maxCalorieDish = menu.stream().max(Comparator.comparing(Dish::getCalories));
maxCalorieDish.ifPresent(dish -> System.out.println("Dish with Maximum calories: " + dish.getName()));

实际上, Stream API提供了maxmin方法, 可以直接获取最大值和最小值。 它们接收一个Comparator作为参数。

代码示例: 找出热量最低的菜肴

Optional<Dish> minCalorieDish = menu.stream().reduce((d1, d2) -> d1.getCalories() < d2.getCalories() ? d1 : d2);minCalorieDish.ifPresent(dish -> System.out.println("Dish with minimum calories: " + dish.getName()));

代码示例(使用方法引用):

    Optional<Dish> minCalorieDish = menu.stream().min(Comparator.comparing(Dish::getCalories));minCalorieDish.ifPresent(dish-> System.out.println("Dish with minimum calories: " + dish.getName()));

同样,也可以使用min方法。

6. 数值流 (Numeric Streams)

在前面的示例中,我们使用 map(Dish::getCalories)Stream<Dish> 转换为 Stream<Integer>,然后进行求和、求最大值等操作。虽然可以工作,但这种方式存在一个潜在的性能问题:自动装箱和拆箱

Integer 是一个对象类型,而 int 是一个基本类型。在 Java 中,将 int 转换为 Integer 称为装箱,将 Integer 转换为 int 称为拆箱。装箱和拆箱操作会带来额外的性能开销,尤其是在处理大量数值数据时。

为了解决这个问题,Java 8 引入了三种 原始类型流特化IntStreamLongStreamDoubleStream,分别用于处理 intlongdouble 类型的数据。这些特化流提供了专门针对数值操作的方法,避免了不必要的装箱和拆箱,从而提高了性能。

6.1 原始类型流特化

IntStreamLongStreamDoubleStream 提供了与 Stream 类似的操作,但它们是专门为数值类型设计的。

优势:

  • 避免自动装箱/拆箱: 直接操作基本类型(intlongdouble),无需转换为对应的对象类型(IntegerLongDouble)。
  • 提供特定数值操作: 提供了 sum()average()min()max()summaryStatistics() 等专门针对数值的操作,这些操作在对象流中需要通过 reduce 等方法实现。

代码示例: 使用 IntStream 计算总热量

int totalCalories = menu.stream().mapToInt(Dish::getCalories) // 将 Stream<Dish> 转换为 IntStream.sum(); // 使用 IntStream 的 sum 方法求和System.out.println("Total calories: " + totalCalories);
  • mapToInt(Dish::getCalories): 这是一个关键步骤。它将 Stream<Dish> 转换为 IntStreammapToInt 方法接收一个 ToIntFunction(函数式接口),该接口将对象映射为 int 值。
  • sum(): IntStream 提供了 sum 方法,直接计算流中所有元素的总和,无需使用 reduce

mapToIntmapToLongmapToDouble

这三个方法分别将对象流转换为对应的原始类型流:

  • mapToInt(ToIntFunction<? super T> mapper): 将流转换为 IntStream
  • mapToLong(ToLongFunction<? super T> mapper): 将流转换为 LongStream
  • mapToDouble(ToDoubleFunction<? super T> mapper): 将流转换为 DoubleStream

boxed():数值流转换为对象流

如果需要将原始类型流转换回对象流(例如,IntStream 转换为 Stream<Integer>),可以使用 boxed() 方法:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed(); // 将 IntStream 转换为 Stream<Integer>

其他数值流操作: 除了sum(),数值流还提供了其他常用的操作:

  • average():计算平均值,返回OptionalDouble
  • max():返回最大值,返回OptionalInt, OptionalLong, OptionalDouble.
  • min():返回最小值,返回OptionalInt, OptionalLong, OptionalDouble.
  • summaryStatistics(): 返回一个IntSummaryStatistics, LongSummaryStatistics DoubleSummaryStatistics对象,其中包含了流中元素的各种统计信息(总和、平均值、最大值、最小值、数量)。

代码示例 (summaryStatistics):

    IntSummaryStatistics statistics = menu.stream().mapToInt(Dish::getCalories).summaryStatistics();System.out.println("Max: " + statistics.getMax());       //最大值System.out.println("Min: " + statistics.getMin());       //最小值System.out.println("Sum: " + statistics.getSum());        //总和System.out.println("Average: " + statistics.getAverage());    //平均值System.out.println("Count: " + statistics.getCount());     //总数
6.2 数值范围

IntStreamLongStream 提供了 rangerangeClosed 静态方法,用于生成指定范围内的数值序列。这在需要生成一系列连续整数时非常有用。

  • IntStream.range(int startInclusive, int endExclusive): 生成一个从 startInclusive(包含)到 endExclusive(不包含)的 int 值序列。
  • IntStream.rangeClosed(int startInclusive, int endInclusive): 生成一个从 startInclusive(包含)到 endInclusive(包含)的 int 值序列。
  • LongStream也有对应的方法。

代码示例: 生成 1 到 100 之间的所有偶数

IntStream evenNumbers = IntStream.rangeClosed(1, 100) // 生成 1 到 100 的整数(包含 1 和 100).filter(n -> n % 2 == 0); // 筛选偶数evenNumbers.forEach(System.out::println);

代码示例: 生成 1 到 10 的整数(不包含 10)

IntStream numbers = IntStream.range(1, 10); // 生成 1 到 9 的整数
numbers.forEach(System.out::println);
6.3 数值流应用:勾股数

让我们通过一个实际的例子来展示数值流的强大功能:生成勾股数。

勾股数是指满足以下条件的三个正整数:a² + b² = c²。

代码示例:

Stream<int[]> pythagoreanTriples =IntStream.rangeClosed(1, 100).boxed() // 生成 1-100 的整数, 并装箱成 Stream<Integer>.flatMap(a ->IntStream.rangeClosed(a, 100) // 对每个 a, 生成 a-100 的整数.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) // 筛选出 b, 使得 a*a + b*b 的平方根是整数.mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a * a + b * b)}) // 将符合条件的 a, b, c 组装成 int 数组);pythagoreanTriples.limit(5).forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));// 可能的输出 (顺序可能不同):
// 3, 4, 5
// 5, 12, 13
// 6, 8, 10
// 7, 24, 25
// 8, 15, 17

代码解释:

  1. IntStream.rangeClosed(1, 100).boxed(): 生成 1 到 100 的整数(包含 1 和 100),并使用 boxed() 方法将 IntStream 转换为 Stream<Integer>。我们需要 Stream<Integer>,因为 flatMap 操作需要返回一个流。
  2. flatMap(a -> ...): 对于 a 的每个值,生成一组勾股数。
  3. IntStream.rangeClosed(a, 100): 生成从 a 到 100 的整数(包含 a 和 100)。我们从 a 开始,因为我们假设 a ≤ b。
  4. filter(b -> Math.sqrt(a*a + b*b) % 1 == 0): 筛选出 b,使得 √(a² + b²) 的结果是整数。% 1 == 0 用于检查一个数是否为整数。
  5. mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a * a + b * b)}): 将符合条件的 abc(√(a² + b²))组装成一个 int 数组。mapToObjIntStream 转换为 Stream<int[]>
  6. limit(5): 限制输出5组。
  7. forEach(...): 打印

优化版本 (避免重复计算平方根):

    Stream<double[]> pythagoreanTriples2 = IntStream.rangeClosed(1, 100).boxed().flatMap(a -> IntStream.rangeClosed(a, 100).mapToObj(b -> new double[]{a, b, Math.sqrt(a * a + b * b)}).filter(t -> t[2] % 1 == 0));

此版本,将a,b以及平方根放入double数组,避免了重复计算。

7. 构建流 (Building Streams)

到目前为止,我们已经学习了如何从集合创建流(例如,使用 stream()parallelStream() 方法),以及如何生成数值范围流(IntStream.rangeIntStream.rangeClosed)。但是,Java 8 流 API 的强大之处远不止于此。它还提供了多种灵活的方式来构建流,包括:

  • 从值直接创建流
  • 从数组创建流
  • 从文件创建流
  • 从函数生成流(无限流
7.1 由值创建流

可以使用 Stream.of 静态方法直接从一组值创建流。

代码示例:

Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
// 输出:
// JAVA 8
// LAMBDAS
// IN
// ACTION

Stream.of 接收可变数量的参数,并创建一个包含这些参数的流。

代码示例 (空流):

Stream<String> emptyStream = Stream.empty();

使用Stream.empty()得到一个空流。

7.2 由数组创建流

可以使用 Arrays.stream 静态方法从数组创建流。

代码示例:

int[] numbers = {2, 3, 5, 7, 11, 13};
IntStream numberStream = Arrays.stream(numbers);
int sum = numberStream.sum();
System.out.println("The sum is: " + sum); // 输出: The sum is: 41

Arrays.stream 方法接收一个数组作为参数,并返回一个与数组元素类型对应的流(例如,int[] 对应 IntStreamString[] 对应 Stream<String>)。

7.3 由文件生成流

Java 8 的 java.nio.file.Files 类提供了许多静态方法来操作文件。其中,Files.lines 方法可以将文件中的每一行作为流中的一个元素,返回一个 Stream<String>

代码示例: 读取文件中的所有行,并计算文件中有多少个不重复的单词

long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) { // data.txt是文件名, 请替换成实际文件名uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) // 将每行分割成单词.distinct() // 去除重复单词.count(); // 计算单词数量
} catch (IOException e) {System.err.println("Error reading file: " + e.getMessage());
}System.out.println("Number of unique words: " + uniqueWords);

注意事项:

  • Files.lines 方法可能会抛出 IOException,需要进行处理(如上例中的 try-catch 块)。
  • 使用 try-with-resources 语句可以确保文件资源在使用后被正确关闭。
  • Paths.get("data.txt") 用于获取文件的路径。你需要将其中的 "data.txt" 替换为实际的文件名。
  • Charset.defaultCharset() 指定文件的字符编码。
7.4 由函数生成流:创建无限流

Java 8 流 API 最令人兴奋的特性之一是能够创建 无限流(infinite streams)。无限流是指没有固定大小、可以无限生成的流。

有两种方法可以创建无限流:

  1. Stream.iterate:

    • 接收一个初始值(种子)。
    • 接收一个 UnaryOperator<T>(一元操作符),用于根据前一个元素生成下一个元素。
  2. Stream.generate:

    • 接收一个 Supplier<T>(供给型函数),用于生成流中的每个元素。
7.4.1 Stream.iterate

Stream.iterate 方法创建一个按需生成元素的流。它不断应用给定的函数来生成新的元素,形成一个无限序列。

代码示例: 生成一个包含前 10 个偶数的流

Stream.iterate(0, n -> n + 2) // 初始值为 0,每次加 2.limit(10) // 限制流的大小为 10.forEach(System.out::println);
// 输出:
// 0
// 2
// 4
// 6
// 8
// 10
// 12
// 14
// 16
// 18
  • Stream.iterate(0, n -> n + 2): 创建一个无限流,初始值为 0,后续元素通过将前一个元素加 2 来生成。
  • limit(10): 非常重要! 由于 iterate 生成的是无限流,如果不使用 limit 进行限制,程序将永远运行下去。limit(10) 将流截断为前 10 个元素。

代码示例: 生成斐波那契数列

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(20).forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));
//输出:
//(0,1)
//(1,1)
//(1,2)
//(2,3)
//(3,5)
//(5,8)
//(8,13)
//(13,21)
//(21,34)
//(34,55)
//(55,89)
//(89,144)
//(144,233)
//(233,377)
//(377,610)
//(610,987)
//(987,1597)
//(1597,2584)
//(2584,4181)
//(4181,6765)
  1. 初始值:new int[]{0, 1}
  2. 根据前一个元素(数组t),生成下一个元素(也是一个数组)
  3. t -> new int[]{t[1], t[0] + t[1]}

如果想输出斐波那契数列的值,而不是元组, 可以利用map操作:

        Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(20).map(t -> t[0]) //只取元祖中的第一个元素.forEach(System.out::println);
7.4.2 Stream.generate

Stream.generate 方法也创建一个无限流,但它不依赖于前一个元素。它接收一个 Supplier<T>,每次需要生成新元素时,都会调用该 Supplier

代码示例: 生成 10 个随机数

Stream.generate(Math::random).limit(10).forEach(System.out::println);
  • Stream.generate(Math::random): 创建一个无限流,每次调用 Math.random() 来生成一个新的随机数。
  • limit(10): 限制流的大小为 10。

代码示例(自定义 Supplier): 创建一个无限流,其中包含递增的整数

IntStream.generate(new IntSupplier() {private int previous = 0;private int current = 1;@Overridepublic int getAsInt() {int oldPrevious = this.previous;int nextValue = this.previous + this.current;this.previous = this.current;this.current = nextValue;return oldPrevious;}}).limit(10).forEach(System.out::println);

这里,我们创建了一个匿名内部类来实现 IntSupplier 接口。getAsInt 方法生成下一个斐波那契数。

重要提示:

  • 无限流是按需生成的,只有在需要元素时才会计算。
  • 由于无限流没有固定大小,因此必须使用 limit 等操作来限制流的大小,否则程序可能会无限运行下去,或者耗尽内存。
  • 无限流在生成测试数据、模拟无限数据源等场景中非常有用。

8. 总结 (Conclusion)

经过本章的学习,你已经掌握了 Java 8 Streams API 中最核心、最实用的操作。从筛选切片,到映射查找匹配,再到归约数值流,你已经能够熟练运用这些“武器”来处理各种数据。更重要的是,你还学会了如何构建流,包括从值、数组、文件,甚至是函数来创建流,打开了通往无限流世界的大门。

让我们再次回顾一下本章的关键知识点:

  • 筛选和切片:
    • filter: 使用谓词(Predicate)筛选符合条件的元素。
    • distinct: 去除流中重复的元素(根据 equalshashCode)。
    • limit: 截取流的前 n 个元素。
    • skip: 跳过流中的前 n 个元素。
  • 映射:
    • map: 将流中的元素转换为另一种形式(一对一映射)。
    • flatMap: 将流中的元素映射为流,并将这些流扁平化为一个流(一对多映射)。
  • 查找和匹配:
    • anyMatch: 检查流中是否至少有一个元素匹配给定的谓词(短路)。
    • allMatch: 检查流中是否所有元素都匹配给定的谓词(短路)。
    • noneMatch: 检查流中是否没有元素匹配给定的谓词(短路)。
    • findAny: 返回流中的任意一个元素(通常用于并行流)。
    • findFirst: 返回流中的第一个元素。
    • Optional: 用于处理可能为空的值,避免 NullPointerException
  • 归约:
    • reduce: 将流中的元素逐步结合起来,产生一个单一的结果(如求和、最大值、最小值)。
  • 数值流:
    • IntStreamLongStreamDoubleStream: 原始类型流特化,避免装箱/拆箱开销。
    • mapToIntmapToLongmapToDouble: 将对象流转换为数值流。
    • boxed: 将数值流转换为对象流。
    • rangerangeClosed: 生成数值范围。
  • 构建流:
    • Stream.of: 从值创建流。
    • Arrays.stream: 从数组创建流。
    • Files.lines: 从文件创建流。
    • Stream.iterate: 从函数生成无限流(需要 limit)。
    • Stream.generate: 从函数生成无限流(需要 limit)。

掌握这些流操作的精髓,你将能够写出更加简洁、高效、易读的数据处理代码。 Stream API 不仅仅是一种新的编程方式,更是一种新的思考方式。它鼓励你将数据处理任务分解为一系列独立的、可组合的操作,从而提高代码的可维护性和可重用性。

进阶学习方向:

  • 并行流: 了解如何利用多核处理器并行处理数据,进一步提升性能。(可参考《Java 8 in Action》第七章)
  • 收集器: 深入学习 Collectors 类提供的各种收集器,以及如何自定义收集器,实现更复杂的数据聚合操作。(可参考《Java 8 in Action》第六章)
  • 流的原理: 探索流的内部实现机制,了解延迟计算、短路等特性的原理。

希望本篇博客能够帮助你更好地掌握 Java 8 Streams API。 让我们一起享受声明式数据处理的乐趣,让代码更优雅,让编程更高效! 如果你有任何问题或建议,欢迎在评论区留言。 感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/69385.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用scoop 下载速度慢怎么办

在国内使用 Scoop 下载速度慢是一个常见问题&#xff0c;主要是因为 Scoop 默认的软件源&#xff08;bucket&#xff09;和下载服务器通常位于国外。以下是一些提高下载速度的方法&#xff1a; 1. 更换 Scoop 镜像源&#xff08;Bucket 镜像&#xff09;&#xff1a; 原理&…

unity学习33:角色相关2,碰撞检测,collider 和 rigidbody,测试一个简单碰撞爆炸效果

目录 1 给gameObject添加rigidbody 2 rigidbody的属性 2.1 基础属性 2.2 插值 详细 2.3 碰撞检测 2.4 constraints 冻结坐标轴的移动和旋转 2.5 layer Overrides 3 碰撞检测 collision Detection 3.1 每个gameObeject上都会创建时自带一个 Collider 3.2 Collider的绿…

DeepSeek-V3:开源多模态大模型的突破与未来

目录 引言 一、DeepSeek-V3 的概述 1.1 什么是 DeepSeek-V3&#xff1f; 1.2 DeepSeek-V3 的定位 二、DeepSeek-V3 的核心特性 2.1 多模态能力 2.2 开源与可扩展性 2.3 高性能与高效训练 2.4 多语言支持 2.5 安全与伦理 三、DeepSeek-V3 的技术架构 3.1 模型架构 3…

警告accumulate and all-reduce gradients in fp32 for bfloat16 data type

这条警告信息是关于分布式训练中的通信优化策略的&#xff0c;具体涉及流水线并行&#xff08;Pipeline Parallelism&#xff09;和点对点通信&#xff08;P2P Communication&#xff09;。以下是对这条警告的详细解释&#xff1a; ### **警告内容** WARNING: Setting args.o…

【生成模型之十四】Visual Autoregressive Modeling

论文&#xff1a;Visual Autoregressive Modeling: Scalable Image Generation via Next-Scale Prediction code&#xff1a;GitHub - FoundationVision/VAR: [NeurIPS 2024 Best Paper][GPT beats diffusion&#x1f525;] [scaling laws in visual generation&#x1f4c8;]…

硬核技术:小程序能够调用手机的哪些传感器

一、加速度传感器 小程序可以调用手机的加速度传感器来检测设备的运动状态。加速度传感器能够测量设备在三个轴&#xff08;X、Y、Z&#xff09;上的加速度变化。通过分析这些数据&#xff0c;小程序可以实现一些功能&#xff0c;如运动检测、步数统计、游戏中的动作感应等。 健…

修剪二叉搜索树(力扣669)

这道题还是比较复杂&#xff0c;在递归上与之前写过的二叉树的题目都有所不同。如果当前递归到的子树的父节点不在范围中&#xff0c;我们根据节点数值的大小选择进行左递归还是右递归。为什么找到了不满足要求的节点之后&#xff0c;还要进行递归呢&#xff1f;因为该不满足要…

活动预告 |【Part 2】Microsoft 安全在线技术公开课:通过扩展检测和响应抵御威胁

课程介绍 通过 Microsoft Learn 免费参加 Microsoft 安全在线技术公开课&#xff0c;掌握创造新机遇所需的技能&#xff0c;加快对 Microsoft Cloud 技术的了解。参加我们举办的“通过扩展检测和响应抵御威胁”技术公开课活动&#xff0c;了解如何更好地在 Microsoft 365 Defen…

【WB 深度学习实验管理】利用 Hugging Face 实现高效的自然语言处理实验跟踪与可视化

本文使用到的 Jupyter Notebook 可在GitHub仓库002文件夹找到&#xff0c;别忘了给仓库点个小心心~~~ https://github.com/LFF8888/FF-Studio-Resources 在自然语言处理领域&#xff0c;使用Hugging Face的Transformers库进行模型训练已经成为主流。然而&#xff0c;随着模型复…

创建一个javaWeb Project

文章目录 前言一、eclipse创建web工程二、web.xmlservlet.xml< mvc:annotation-driven/ > Spring MVC 驱动< context:component - scan >&#xff1a;扫描< bean > ... < /bean >< import > config/beans.xml beans.xmlmybatis.xml 前言 javaWe…

【蓝桥杯—单片机】第十一届省赛真题代码题解题笔记 | 省赛 | 真题 | 代码题 | 刷题 | 笔记

第十一届省赛真题代码部分 前言赛题代码思路笔记竞赛板配置内部振荡器频率设定键盘工作模式跳线扩展方式跳线 建立模板明确设计要求和初始状态显示功能部分数据界面第一部分第二部分第三部分调试时发现的问题 参数设置界面第一部分第二部分和第四部分第三部分和第五部分 按键功…

寒假2.7

题解 web&#xff1a;[HCTF 2018]WarmUp 打开是张表情包 看一下源代码 访问source.php&#xff0c;得到完整代码 代码审计 <?phphighlight_file(__FILE__);class emmm{public static function checkFile(&$page){$whitelist ["source">"source.p…

【LeetCode Hot100 动态规划】

动态规划 动态规划五部曲简单动态规划问题爬楼梯打家劫舍 01背包类问题01背包基础二维动态数组一维动态数组分割等和子集 完全背包类问题完全背包基础零钱兑换完全平方数零钱兑换II组合总和IV单词拆分 子序列问题最长递增子序列乘积最大子数组 动态规划五部曲 确定dp数组&…

python康威生命游戏的图形化界面实现

康威生命游戏&#xff08;Conway’s Game of Life&#xff09;是由英国数学家约翰何顿康威&#xff08;John Horton Conway&#xff09;在1970年发明的一款零玩家的细胞自动机模拟游戏。尽管它的名字中有“游戏”&#xff0c;但实际上它并不需要玩家参与操作&#xff0c;而是通…

【数据结构】链表应用-链表重新排序

重新排序 反转链表预期实现思路解题过程code力扣代码核心代码完整代码 总结 删除链表中间节点代码解惑 链表重新排序题目描述解题思路解题过程复杂度代码力扣代码完整代码 反转链表 预期实现 思路 你选用何种方法解题&#xff1f; 我选用了迭代法来反转链表。这是一种经典且高…

使用mockttp库模拟HTTP服务器和客户端进行单元测试

简介 mockttp 是一个用于在 Node.js 中模拟 HTTP 服务器和客户端的库。它可以帮助我们进行单元测试和集成测试&#xff0c;而不需要实际发送 HTTP 请求。 安装 npm install mockttp types/mockttp模拟http服务测试 首先导入并创建一个本地服务器实例 import { getLocal } …

pytest-xdist 进行多进程并发测试!

在软件开发过程中&#xff0c;测试是确保代码质量和可靠性的关键步骤。随着项目规模的扩大和复杂性的增加&#xff0c;测试用例的执行效率变得尤为重要。为了加速测试过程&#xff0c;特别是对于一些可以并行执行的测试用 例&#xff0c;pytest-xdist 提供了一种强大的工具&…

mysql8安装时提示-缺少Microsoft Visual C++ 2019 x64 redistributable

MySQL8.0安装包mysql-8.0.1-winx64进行安装&#xff0c;提示&#xff1a;This application requires Visual Studio 2019 x64Redistributable, Please install the Redistributable then runthis installer again。出现这个错误是因为我们电脑缺少Microsoft Visual C 这个程序&…

基于HTML生成网页有什么优势

在互联网时代&#xff0c;网页是人们获取信息、交流互动的重要窗口&#xff0c;而基于HTML生成网页&#xff0c;是搭建网络大厦的关键。HTML语法简洁直观&#xff0c;标签和属性语义明确&#xff0c;新手也能迅速上手&#xff0c;创建包含基础元素的网页&#xff0c;极大降低了…

【MySQL】深度理解事务的隔离性:全面讲解事务的四种隔离级别

**前言&#xff1a;**上节内容我们主要说了如果没有设置保存点&#xff0c; 也可以回滚&#xff0c;但是只能回滚到事务的开始。直接使用rollback的前提是事务还没有提交。并且如果一个事务被提交了&#xff0c;就不可以回退。同时我们也可以使用savepoint设置回滚点。 可以自己…