前言
在现代 Java 编程中,Stream 已经成为一个强大的工具,用于集合操作和数据处理。它引入了一种更具表现力和简洁性的方法,使你能够以声明性方式操作数据。本篇博客将带你深入探讨 Java Stream,从基础概念到高级技巧,你将了解如何使用它来提高代码的可读性和效率。
1️⃣ :Java Stream 基础
Java Stream 是 Java 8 引入的一种用于处理集合数据的抽象概念。它提供了一种更高层次的、函数式的方法来操作数据,允许开发者以更简洁的方式执行各种集合操作。Java Stream 的主要特点包括惰性计算、链式操作以及并行处理数据的能力。
以下是 Java Stream 的基础内容:
创建 Stream
你可以通过多种方式创建 Java Stream:
- 从集合创建:你可以从集合类(如List、Set、Map)创建Stream,使用
stream()
方法或parallelStream()
方法。例如:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Stream<String> stream = names.stream();
- 使用
Stream.of()
方法:你可以使用Stream.of()
方法创建包含指定元素的Stream。例如:
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
- 通过数组创建:使用
Arrays.stream()
方法,你可以将数组转换为Stream。例如:
int[] array = {1, 2, 3, 4, 5}; IntStream intStream = Arrays.stream(array);
基本操作
一旦创建了Stream,你可以执行各种操作,包括:
- 中间操作: 这些操作允许你在数据上进行处理,但不会触发实际计算。常见的中间操作包括
filter
(过滤)、map
(映射)、distinct
(去重)等。
Stream<String> filteredNames = names.stream() .filter(name -> name.length() > 4) .map(name -> name.toUpperCase()) .distinct();
- 终端操作: 这些操作会触发Stream的计算,返回结果或副作用。常见的终端操作包括
forEach
(遍历)、collect
(收集为集合)、reduce
(归约)等。
filteredNames.forEach(System.out::println); List<String> collectedNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList()); int sum = IntStream.of(1, 2, 3, 4, 5) .reduce(0, (a, b) -> a + b);
示例
以下是一个示例,演示了如何创建Stream,进行过滤、映射和遍历操作:
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class StreamExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); // 创建Stream Stream<String> stream = names.stream(); // 过滤、映射、去重 List<String> filteredAndMappedNames = stream .filter(name -> name.length() > 4) .map(String::toUpperCase) .distinct() .collect(Collectors.toList()); // 遍历结果 filteredAndMappedNames.forEach(System.out::println); } }
这段代码从名字列表创建了一个Stream,然后对其进行过滤、映射、去重等操作,并最终将结果收集为一个列表。最后,使用forEach
方法遍历输出结果。
Java Stream 提供了强大的功能,允许你以函数式编程的方式处理集合数据,提高了代码的简洁性和可读性。你可以根据需要组合各种操作来处理数据。
2️⃣:Stream操作
Java Stream 操作可以分为两类:中间操作和终端操作。中间操作用于构建操作链,但不会触发计算,而终端操作会触发计算并返回结果。你可以通过链式方式组合这些操作来处理数据。以下是关于这些操作的详细讨论:
中间操作
filter(Predicate<T> predicate)
: 这个操作用于过滤Stream中的元素。它接受一个谓词(Predicate)作为参数,谓词用于确定是否保留每个元素。只有满足谓词条件的元素才会保留在Stream中。map(Function<T, R> mapper)
: 用于将Stream中的元素映射为另一种类型。它接受一个映射函数(Function)作为参数,该函数将每个元素转换为另一种类型。flatMap(Function<T, Stream<R>> mapper)
: 用于将每个输入元素映射为一个Stream,然后将这些Stream合并为一个Stream。通常用于将嵌套结构的数据展开。distinct()
: 用于去重操作,去掉重复的元素,保留唯一的元素。sorted()
: 对Stream中的元素进行排序。你可以选择使用自然排序或提供自定义的比较器。peek(Consumer<T> action)
: 用于在处理元素时执行某个操作,通常用于调试或记录中间状态。
终端操作
forEach(Consumer<T> action)
: 遍历Stream中的每个元素,并对每个元素执行给定的操作。collect(Collector<T, A, R> collector)
: 将Stream中的元素收集为一个集合,如List、Set、Map等,使用指定的收集器(Collector)。toArray()
: 将Stream中的元素转换为数组。reduce(identity, BinaryOperator<T> accumulator)
: 进行归约操作,将Stream中的元素逐个聚合,返回一个结果。min(Comparator<T> comparator)
和max(Comparator<T> comparator)
: 返回Stream中的最小和最大元素,根据给定的比较器。count()
: 返回Stream中的元素个数。anyMatch(Predicate<T> predicate)
和allMatch(Predicate<T> predicate)
: 用于检查是否有元素匹配给定的谓词,或者是否所有元素都匹配。findAny()
和findFirst()
: 返回Stream中的任意一个元素或第一个元素。min()
和max()
: 返回Stream中的最小和最大元素,使用元素的自然顺序。sum()、average()、summaryStatistics()
: 用于计算元素的总和、平均值和统计信息。toArray()
: 将Stream中的元素转换为数组。
链式操作
Java Stream 允许你链式组合多个操作,例如:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); List<String> result = names.stream() .filter(name -> name.length() > 4) .map(String::toUpperCase) .distinct() .collect(Collectors.toList());
在上述示例中,我们创建了一个Stream,然后使用filter
、map
、distinct
和collect
操作链式组合,以过滤、映射、去重和收集数据。
这种链式操作的方式使代码更加清晰和简洁,同时也支持延迟计算,只有在终端操作时才触发实际计算,这有助于提高性能。
3️⃣:Lambda 表达式和函数式接口
Lambda 表达式和函数式接口是 Java 8 引入的特性,与 Stream 结合使用可以实现更灵活和简洁的数据处理。下面是如何使用 Lambda 表达式和函数式接口与 Stream 一起工作的解释:
Lambda 表达式
Lambda 表达式是一种轻量级的匿名函数,它可以用来传递行为,通常用于函数式编程的上下文中。在 Stream 操作中,Lambda 表达式常用于定义中间和终端操作的行为。
示例 1:使用 Lambda 表达式过滤元素
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); // 使用Lambda表达式过滤名字长度大于4的元素 List<String> result = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());
函数式接口
函数式接口是一个只包含一个抽象方法的接口,它用于传递 Lambda 表达式的类型。Java 8 中提供了许多内置的函数式接口,如Predicate
、Function
、Consumer
等,它们可以用于定义 Lambda 表达式的参数和返回类型。
示例 2:使用函数式接口 Predicate 进行过滤
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); // 使用 Predicate 过滤名字长度大于4的元素 Predicate<String> filterByLength = name -> name.length() > 4; List<String> result = names.stream() .filter(filterByLength) .collect(Collectors.toList());
自定义函数式接口
你还可以创建自定义的函数式接口,以满足特定需求。例如,如果需要将两个参数合并成一个结果,可以创建一个接受两个参数的函数式接口。
示例 3:使用自定义函数式接口进行操作
@FunctionalInterface interface StringCombiner { String combine(String s1, String s2); } public class StreamExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); // 使用自定义函数式接口合并名字 StringCombiner combiner = (s1, s2) -> s1 + " and " + s2; String combined = names.stream() .reduce("", combiner::combine); System.out.println(combined); } }
上述示例中,我们定义了自定义的函数式接口StringCombiner
,用于将两个字符串合并。然后,我们使用该函数式接口在 Stream 操作中进行字符串的合并。
通过结合 Lambda 表达式和函数式接口,你可以以更灵活、清晰和简洁的方式处理数据,自定义操作的行为,以及实现各种数据处理需求。这使得 Stream 在 Java 中变得非常强大和易用。
4️⃣:过滤、映射和归约
常见的 Stream 操作包括过滤、映射(map)和归约(reduce),它们是 Stream 处理数据的核心操作。下面分别介绍这些操作,并提供示例:
1. 过滤 (Filter): 过滤是一种中间操作,它允许你基于某个条件来筛选出 Stream 中的元素,只保留符合条件的元素。
示例:过滤出长度大于4的名字。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); List<String> filteredNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());
2. 映射 (Map): 映射是一种中间操作,它允许你将 Stream 中的每个元素映射成另一种类型。常用于数据类型转换或提取元素的某个属性。
示例:将名字列表转换为大写形式。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); List<String> upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList());
3. 归约 (Reduce): 归约是一种终端操作,它允许你将 Stream 中的所有元素合并为一个结果。通常用于求和、求最大值、求最小值等聚合操作。
示例:计算所有数字的总和。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (a, b) -> a + b);
4. 其他操作: 除了上述操作,Stream 还提供了许多其他操作,如去重(distinct)、排序(sorted)、查找(findAny、findFirst)、匹配(anyMatch、allMatch)、统计(count)等,这些操作可以根据需求进行组合使用。
示例:使用多个操作组合,过滤出长度大于4的名字,将它们映射为大写形式,并计算它们的个数。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); long count = names.stream() .filter(name -> name.length() > 4) .map(String::toUpperCase) .count();
这些操作使得 Stream 在处理数据时变得非常强大和灵活。你可以根据具体需求组合这些操作,以实现各种数据处理任务。此外,Stream 还支持并行处理,提高了性能,特别是在处理大量数据时。
5️⃣:并行流处理
并行流处理是 Java 8 引入的一个功能,允许并行处理数据以提高性能。它允许将操作并行化,使多个处理单元同时处理数据,特别适用于多核处理器。以下是关于如何使用并行流处理以及何时应该选择并行处理的信息:
使用并行流处理:
你可以将串行的 Stream 转换为并行流,以便在多个处理单元上并行执行操作。在 Stream 上使用 parallel()
方法可以实现这一转换。
示例:将串行流转换为并行流。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Stream<Integer> parallelStream = numbers.stream().parallel();
在并行流上执行操作的方式与串行流基本相同,只是操作会在多个线程上并行执行,以提高处理速度。
何时选择并行处理:
选择并行处理取决于数据量和操作的性质。以下是一些情况下选择并行处理的建议:
- 大数据集: 当数据集足够大时,使用并行流可以充分发挥多核处理器的性能优势。对于小数据集,并行处理可能会引入额外的开销,不如串行处理高效。
- 密集的计算: 如果操作是密集的计算,例如复杂的数学运算,使用并行流可以加速处理。这对于并行处理非常有帮助。
- I/O 操作: 当操作涉及到 I/O 操作(如读写文件或网络通信),并行流可以减少 I/O 操作的等待时间,提高处理效率。
- 任务可以独立执行: 并行处理要求每个任务是独立的,不涉及共享状态或数据竞争。这确保了并行处理的正确性。
- 数据可分割: 数据应该能够被分割成多个子任务,以便并行执行。例如,一个大型集合可以分成多个子集合,每个子集合可以由不同的线程处理。
注意事项:
- 并行流处理可能引入线程管理和同步的开销。因此,在某些情况下,并不一定比串行流更快。
- 并行处理应谨慎使用,特别是在涉及共享状态、线程安全和复杂的同步问题时。确保处理的数据是线程安全的,或者使用适当的同步机制来保护共享数据。
- 在使用并行流之前,进行性能测试以确保它确实提高了处理速度,不会引入不必要的复杂性。
总之,选择是否使用并行处理取决于具体情况。对于大数据集和适合并行的操作,使用并行流可以显著提高性能。然而,在处理小数据集或复杂同步需求的情况下,可能更适合使用串行流。在实际应用中,根据任务的性质和数据的规模来选择合适的流处理方式是很重要的。
6️⃣:Stream 的高级操作
Stream 提供了许多高级操作,使数据处理更加灵活和强大。以下是一些高级操作,包括 flatMap
、收集器(Collectors
)、分组、分区等:
1. flatMap
: flatMap
操作用于将多个流合并为一个流,通常用于处理嵌套数据结构。
示例:将多个字符串列表合并为一个列表。
List<List<String>> nestedLists = Arrays.asList( Arrays.asList("Alice", "Bob"), Arrays.asList("Charlie", "David"), Arrays.asList("Eva", "Frank") ); List<String> flatList = nestedLists.stream() .flatMap(List::stream) .collect(Collectors.toList());
2. 收集器(Collectors
): 收集器是用于将 Stream 中的元素收集为集合或值的工具。Java 提供了许多内置的收集器,如 toList
、toSet
、toMap
等,你还可以创建自定义的收集器。
示例:将字符串列表收集为 Set。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); Set<String> nameSet = names.stream() .collect(Collectors.toSet());
3. 分组: 分组操作允许你将 Stream 中的元素按照某个属性或条件分组成一个 Map。
示例:按名字的长度分组成 Map。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); Map<Integer, List<String>> groupedNames = names.stream() .collect(Collectors.groupingBy(String::length));
4. 分区: 分区操作是一种特殊的分组操作,它将元素分为两个分区,满足条件的一组,不满足条件的另一组。
示例:将名字分为长度大于4和不大于4的两组。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); Map<Boolean, List<String>> partitionedNames = names.stream() .collect(Collectors.partitioningBy(name -> name.length() > 4));
5. 自定义收集器: 你可以创建自定义的收集器来满足特定需求,这通常涉及实现 Collector
接口的方法。
示例:自定义收集器,将名字长度大于4的以逗号分隔的字符串收集起来。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); String concatenatedNames = names.stream() .filter(name -> name.length() > 4) .collect(CustomCollector.joining(", "));
6. 排序和限制: 使用 sorted
操作对元素进行排序,并使用 limit
操作限制结果的数量。
示例:对数字进行排序并限制结果数量。
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 2, 7, 4, 6); List<Integer> sortedNumbers = numbers.stream() .sorted() .limit(4) .collect(Collectors.toList());
这些高级操作使 Stream 更加强大和灵活,可以满足各种复杂的数据处理需求。你可以根据具体情况组合这些操作,以实现不同的数据处理任务。
7️⃣:Stream的异常处理
在处理 Stream 操作时,有时候可能会遇到引发异常的情况,如空指针异常、除零异常等。为确保代码的健壮性,可以采取以下方法来处理可能的异常情况:
1. 使用 filter
进行预处理: 在进行一些操作之前,可以使用 filter
过滤掉不符合条件的元素,从而减少可能引发异常的机会。例如,在进行除法操作之前,可以过滤掉分母为零的情况。
List<Integer> numbers = Arrays.asList(1, 2, 0, 3, 4, 0); List<Double> result = numbers.stream() .filter(num -> num != 0) .map(num -> 10.0 / num) .collect(Collectors.toList());
2. 使用 Optional
类: Optional
是一种用于处理可能为空(null)值的容器类,它可以帮助避免空指针异常。你可以使用 Optional
包装可能为 null 的值,然后安全地操作这些值。
List<String> names = Arrays.asList("Alice", "Bob", null, "David", null, "Eva"); List<String> validNames = names.stream() .filter(Objects::nonNull) .map(name -> name.toUpperCase()) .collect(Collectors.toList());
3. 使用异常处理: 在一些情况下,你可能无法完全避免异常,例如,当处理外部资源时(文件、网络连接等)。在这种情况下,可以使用异常处理机制,如 try-catch
块来捕获和处理异常。
try { List<String> lines = Files.readAllLines(Paths.get("file.txt")); } catch (IOException e) { e.printStackTrace(); }
4. 使用 orElse
和 orElseThrow
: 如果你希望为可能为空的值提供默认值,可以使用 orElse
方法。如果值为空,则返回默认值。
Optional<String> name = Optional.ofNullable(getNameFromExternalSource()); String result = name.orElse("Unknown");
另外,你还可以使用 orElseThrow
方法在值为空时抛出自定义异常。
Optional<String> name = Optional.ofNullable(getNameFromExternalSource()); String result = name.orElseThrow(() -> new RuntimeException("Name not found"));
5. 避免链式操作中的异常: 如果你的 Stream 操作链式很长,可能难以在其中正确处理异常。在这种情况下,可以拆分链式操作,以便在合适的地方进行异常处理。
List<String> names = Arrays.asList("Alice", "Bob", null, "David", null, "Eva"); List<String> validNames = names.stream() .filter(Objects::nonNull) .map(name -> name.toUpperCase()) .filter(name -> name.startsWith("D")) .collect(Collectors.toList());
这样,你可以在合适的地方添加异常处理,以提高代码的可读性和健壮性。
总之,在处理可能引发异常的 Stream 操作时,建议采取预处理、使用 Optional
、异常处理或拆分操作链等方法,以确保代码的健壮性和可靠性。具体处理方式取决于具体情况和需求。
8️⃣:性能和最佳实践:
Stream 提供了强大的功能,但在使用它们时需要注意性能和最佳实践,以确保代码高效和易于维护。以下是一些关于 Stream 的性能和最佳实践的指导:
1. 避免不必要的装箱和拆箱: 当你在 Stream 操作中使用基本数据类型时,避免不必要的装箱和拆箱操作,这可以提高性能。可以使用mapToXxx
操作,如 mapToInt
、mapToDouble
等,将流元素映射为相应的基本数据类型。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .mapToInt(Integer::intValue) // 避免装箱操作 .sum();
2. 使用并行流: 当处理大量数据时,可以考虑使用并行流以充分发挥多核处理器的性能。但要谨慎使用,并确保代码正确性,特别是在并行操作中可能涉及共享状态的情况下。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); int sum = numbers.parallelStream() .mapToInt(Integer::intValue) .sum();
3. 使用无状态操作: Stream 提供了无状态和有状态的操作。无状态操作是那些每个元素的处理都不依赖于其他元素,通常可以更容易地并行化。无状态操作包括 map
、filter
、flatMap
等。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); long count = numbers.stream() .map(num -> num * 2) // 无状态操作 .count();
4. 使用延迟操作: Stream 具有延迟计算的特性,只有在终端操作触发时才执行实际计算。这使得可以构建更灵活的操作链。但要小心不要在终端操作之前触发计算,以避免不必要的开销。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); List<String> result = names.stream() .filter(name -> name.length() > 4) .map(String::toUpperCase) .collect(Collectors.toList());
5. 使用适当的数据结构: 在某些情况下,选择适当的数据结构可以提高性能。例如,如果需要频繁插入和删除元素,考虑使用 LinkedList
而不是 ArrayList
。
6. 谨慎使用无界流: 无界流(如 Stream.generate
和 Stream.iterate
)可能会导致无限数据量的流,要小心使用,确保不会陷入无限循环。
Stream<Integer> infiniteStream = Stream.generate(() -> 1); List<Integer> limitedList = infiniteStream.limit(100).collect(Collectors.toList());
7. 测试和性能优化: 性能优化需要在具体情况下进行测试和分析,以找到瓶颈并进行改进。Java 提供了一些性能分析工具,如 VisualVM 和 Profiler,用于分析代码性能。
最佳实践和性能优化取决于具体情况和需求,因此在编写和维护代码时,建议进行性能测试和分析,以确保代码既高效又可维护。遵循上述最佳实践可以帮助你更好地利用 Java 8 中的 Stream 功能。