文章目录
- 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
操作返回一个新流,其中包含原始流中所有不重复的元素(根据 equals
和 hashCode
方法判断)。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
注意事项: 对于自定义对象,需要正确实现 equals
和 hashCode
方法,才能确保 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
擅长处理嵌套的流结构,可以将多个流“拍扁”成一个流。
用图示说明 map
和 flatMap
的区别:
假设我们有一个包含多个列表的流: 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]
map(sentence -> sentence.split(" "))
: 将每个句子分割成单词数组。此时,流的类型是Stream<String[]>
。flatMap(Arrays::stream)
:Arrays::stream
将每个String[]
数组转换为一个Stream<String>
。flatMap
将这些流合并成一个Stream<String>
。distinct()
: 去除重复的单词。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)
- 内层map, 生成数对。
- 外层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
必须等待第一个元素可用。 - 顺序流: 在顺序流中,
findAny
和findFirst
通常返回相同的结果(第一个元素)。但是,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
类型详解:
findAny
和 findFirst
都返回 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
方法最常见的用法之一就是对流中的数值元素进行求和。它有两种重载形式:
-
reduce(T identity, BinaryOperator<T> accumulator)
:identity
: 初始值(累加器的起点)。accumulator
: 一个BinaryOperator<T>
(二元操作符),它接收两个参数(累加器的当前值和流中的下一个元素),并返回一个新的累加值。
-
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提供了max
和min
方法, 可以直接获取最大值和最小值。 它们接收一个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 引入了三种 原始类型流特化:IntStream
、LongStream
和 DoubleStream
,分别用于处理 int
、long
和 double
类型的数据。这些特化流提供了专门针对数值操作的方法,避免了不必要的装箱和拆箱,从而提高了性能。
6.1 原始类型流特化
IntStream
、LongStream
和 DoubleStream
提供了与 Stream
类似的操作,但它们是专门为数值类型设计的。
优势:
- 避免自动装箱/拆箱: 直接操作基本类型(
int
、long
、double
),无需转换为对应的对象类型(Integer
、Long
、Double
)。 - 提供特定数值操作: 提供了
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>
转换为IntStream
。mapToInt
方法接收一个ToIntFunction
(函数式接口),该接口将对象映射为int
值。sum()
:IntStream
提供了sum
方法,直接计算流中所有元素的总和,无需使用reduce
。
mapToInt
、mapToLong
、mapToDouble
:
这三个方法分别将对象流转换为对应的原始类型流:
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 数值范围
IntStream
和 LongStream
提供了 range
和 rangeClosed
静态方法,用于生成指定范围内的数值序列。这在需要生成一系列连续整数时非常有用。
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
代码解释:
IntStream.rangeClosed(1, 100).boxed()
: 生成 1 到 100 的整数(包含 1 和 100),并使用boxed()
方法将IntStream
转换为Stream<Integer>
。我们需要Stream<Integer>
,因为flatMap
操作需要返回一个流。flatMap(a -> ...)
: 对于a
的每个值,生成一组勾股数。IntStream.rangeClosed(a, 100)
: 生成从a
到 100 的整数(包含a
和 100)。我们从a
开始,因为我们假设 a ≤ b。filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
: 筛选出b
,使得 √(a² + b²) 的结果是整数。% 1 == 0
用于检查一个数是否为整数。mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a * a + b * b)})
: 将符合条件的a
、b
和c
(√(a² + b²))组装成一个int
数组。mapToObj
将IntStream
转换为Stream<int[]>
。limit(5)
: 限制输出5组。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.range
、IntStream.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[]
对应 IntStream
,String[]
对应 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)。无限流是指没有固定大小、可以无限生成的流。
有两种方法可以创建无限流:
-
Stream.iterate
:- 接收一个初始值(种子)。
- 接收一个
UnaryOperator<T>
(一元操作符),用于根据前一个元素生成下一个元素。
-
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)
- 初始值:
new int[]{0, 1}
- 根据前一个元素(数组
t
),生成下一个元素(也是一个数组) 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
: 去除流中重复的元素(根据equals
和hashCode
)。limit
: 截取流的前 n 个元素。skip
: 跳过流中的前 n 个元素。
- 映射:
map
: 将流中的元素转换为另一种形式(一对一映射)。flatMap
: 将流中的元素映射为流,并将这些流扁平化为一个流(一对多映射)。
- 查找和匹配:
anyMatch
: 检查流中是否至少有一个元素匹配给定的谓词(短路)。allMatch
: 检查流中是否所有元素都匹配给定的谓词(短路)。noneMatch
: 检查流中是否没有元素匹配给定的谓词(短路)。findAny
: 返回流中的任意一个元素(通常用于并行流)。findFirst
: 返回流中的第一个元素。Optional
: 用于处理可能为空的值,避免NullPointerException
。
- 归约:
reduce
: 将流中的元素逐步结合起来,产生一个单一的结果(如求和、最大值、最小值)。
- 数值流:
IntStream
、LongStream
、DoubleStream
: 原始类型流特化,避免装箱/拆箱开销。mapToInt
、mapToLong
、mapToDouble
: 将对象流转换为数值流。boxed
: 将数值流转换为对象流。range
、rangeClosed
: 生成数值范围。
- 构建流:
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。 让我们一起享受声明式数据处理的乐趣,让代码更优雅,让编程更高效! 如果你有任何问题或建议,欢迎在评论区留言。 感谢阅读!