深入 Java Stream:高级流操作和技巧

简介: 深入 Java Stream:高级流操作和技巧


前言

在现代 Java 编程中,Stream 已经成为一个强大的工具,用于集合操作和数据处理。它引入了一种更具表现力和简洁性的方法,使你能够以声明性方式操作数据。本篇博客将带你深入探讨 Java Stream,从基础概念到高级技巧,你将了解如何使用它来提高代码的可读性和效率。

1️⃣ :Java Stream 基础

Java Stream 是 Java 8 引入的一种用于处理集合数据的抽象概念。它提供了一种更高层次的、函数式的方法来操作数据,允许开发者以更简洁的方式执行各种集合操作。Java Stream 的主要特点包括惰性计算、链式操作以及并行处理数据的能力。

以下是 Java Stream 的基础内容:

创建 Stream

你可以通过多种方式创建 Java Stream:

  1. 从集合创建:你可以从集合类(如List、Set、Map)创建Stream,使用stream()方法或parallelStream()方法。例如:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();
  1. 使用Stream.of()方法:你可以使用Stream.of()方法创建包含指定元素的Stream。例如:
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
  1. 通过数组创建:使用Arrays.stream()方法,你可以将数组转换为Stream。例如:
int[] array = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(array);

基本操作

一旦创建了Stream,你可以执行各种操作,包括:

  1. 中间操作: 这些操作允许你在数据上进行处理,但不会触发实际计算。常见的中间操作包括filter(过滤)、map(映射)、distinct(去重)等。
Stream<String> filteredNames = names.stream()
    .filter(name -> name.length() > 4)
    .map(name -> name.toUpperCase())
    .distinct();
  1. 终端操作: 这些操作会触发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 操作可以分为两类:中间操作和终端操作。中间操作用于构建操作链,但不会触发计算,而终端操作会触发计算并返回结果。你可以通过链式方式组合这些操作来处理数据。以下是关于这些操作的详细讨论:

中间操作

  1. filter(Predicate<T> predicate) 这个操作用于过滤Stream中的元素。它接受一个谓词(Predicate)作为参数,谓词用于确定是否保留每个元素。只有满足谓词条件的元素才会保留在Stream中。
  2. map(Function<T, R> mapper) 用于将Stream中的元素映射为另一种类型。它接受一个映射函数(Function)作为参数,该函数将每个元素转换为另一种类型。
  3. flatMap(Function<T, Stream<R>> mapper) 用于将每个输入元素映射为一个Stream,然后将这些Stream合并为一个Stream。通常用于将嵌套结构的数据展开。
  4. distinct() 用于去重操作,去掉重复的元素,保留唯一的元素。
  5. sorted() 对Stream中的元素进行排序。你可以选择使用自然排序或提供自定义的比较器。
  6. peek(Consumer<T> action) 用于在处理元素时执行某个操作,通常用于调试或记录中间状态。

终端操作

  1. forEach(Consumer<T> action) 遍历Stream中的每个元素,并对每个元素执行给定的操作。
  2. collect(Collector<T, A, R> collector) 将Stream中的元素收集为一个集合,如List、Set、Map等,使用指定的收集器(Collector)。
  3. toArray() 将Stream中的元素转换为数组。
  4. reduce(identity, BinaryOperator<T> accumulator) 进行归约操作,将Stream中的元素逐个聚合,返回一个结果。
  5. min(Comparator<T> comparator)max(Comparator<T> comparator) 返回Stream中的最小和最大元素,根据给定的比较器。
  6. count() 返回Stream中的元素个数。
  7. anyMatch(Predicate<T> predicate)allMatch(Predicate<T> predicate) 用于检查是否有元素匹配给定的谓词,或者是否所有元素都匹配。
  8. findAny()findFirst() 返回Stream中的任意一个元素或第一个元素。
  9. min()max() 返回Stream中的最小和最大元素,使用元素的自然顺序。
  10. sum()、average()、summaryStatistics() 用于计算元素的总和、平均值和统计信息。
  11. 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,然后使用filtermapdistinctcollect操作链式组合,以过滤、映射、去重和收集数据。

这种链式操作的方式使代码更加清晰和简洁,同时也支持延迟计算,只有在终端操作时才触发实际计算,这有助于提高性能。

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 中提供了许多内置的函数式接口,如PredicateFunctionConsumer等,它们可以用于定义 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();

在并行流上执行操作的方式与串行流基本相同,只是操作会在多个线程上并行执行,以提高处理速度。

何时选择并行处理:

选择并行处理取决于数据量和操作的性质。以下是一些情况下选择并行处理的建议:

  1. 大数据集: 当数据集足够大时,使用并行流可以充分发挥多核处理器的性能优势。对于小数据集,并行处理可能会引入额外的开销,不如串行处理高效。
  2. 密集的计算: 如果操作是密集的计算,例如复杂的数学运算,使用并行流可以加速处理。这对于并行处理非常有帮助。
  3. I/O 操作: 当操作涉及到 I/O 操作(如读写文件或网络通信),并行流可以减少 I/O 操作的等待时间,提高处理效率。
  4. 任务可以独立执行: 并行处理要求每个任务是独立的,不涉及共享状态或数据竞争。这确保了并行处理的正确性。
  5. 数据可分割: 数据应该能够被分割成多个子任务,以便并行执行。例如,一个大型集合可以分成多个子集合,每个子集合可以由不同的线程处理。

注意事项:

  1. 并行流处理可能引入线程管理和同步的开销。因此,在某些情况下,并不一定比串行流更快。
  2. 并行处理应谨慎使用,特别是在涉及共享状态、线程安全和复杂的同步问题时。确保处理的数据是线程安全的,或者使用适当的同步机制来保护共享数据。
  3. 在使用并行流之前,进行性能测试以确保它确实提高了处理速度,不会引入不必要的复杂性。

总之,选择是否使用并行处理取决于具体情况。对于大数据集和适合并行的操作,使用并行流可以显著提高性能。然而,在处理小数据集或复杂同步需求的情况下,可能更适合使用串行流。在实际应用中,根据任务的性质和数据的规模来选择合适的流处理方式是很重要的。

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 提供了许多内置的收集器,如 toListtoSettoMap 等,你还可以创建自定义的收集器。

示例:将字符串列表收集为 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. 使用 orElseorElseThrow 如果你希望为可能为空的值提供默认值,可以使用 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操作,如 mapToIntmapToDouble 等,将流元素映射为相应的基本数据类型。

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 提供了无状态和有状态的操作。无状态操作是那些每个元素的处理都不依赖于其他元素,通常可以更容易地并行化。无状态操作包括 mapfilterflatMap 等。

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.generateStream.iterate)可能会导致无限数据量的流,要小心使用,确保不会陷入无限循环。

Stream<Integer> infiniteStream = Stream.generate(() -> 1);
List<Integer> limitedList = infiniteStream.limit(100).collect(Collectors.toList());

7. 测试和性能优化: 性能优化需要在具体情况下进行测试和分析,以找到瓶颈并进行改进。Java 提供了一些性能分析工具,如 VisualVM 和 Profiler,用于分析代码性能。

最佳实践和性能优化取决于具体情况和需求,因此在编写和维护代码时,建议进行性能测试和分析,以确保代码既高效又可维护。遵循上述最佳实践可以帮助你更好地利用 Java 8 中的 Stream 功能。

相关文章
|
18天前
|
Oracle 架构师 Java
Java 22 新增利器: 使用 Java Stream Gather 优雅地处理流中的状态
本文中我们分析了 什么 是 “流”,对比了 Java 上几种常见的 “流”库,引入和详细介绍了 Java 22 中的 Stream Gather API 。同时也简单分享了利用虚拟线程 如何简化 Stream map Concurrent操作符的实现。希望抛砖引玉和大家分享新的特性,共同进步。同时也希望大家都可以升级到新版本的 JDK,更好的赋能业务。
|
4天前
|
Java 编译器 API
Java基础教程(17)-Java8中的lambda表达式和Stream、Optional
【4月更文挑战第17天】Lambda表达式是Java 8引入的函数式编程特性,允许函数作为参数或返回值。它有简洁的语法:`(parameters) -> expression 或 (parameters) ->{ statements; }`。FunctionalInterface注解用于标记单方法接口,可以用Lambda替换。
|
5天前
|
存储 SQL Java
JavaSE&Java8 Stream
JavaSE&Java8 Stream
|
14天前
|
存储 安全 Java
说说Java 8 引入的Stream API
说说Java 8 引入的Stream API
14 0
|
14天前
|
分布式计算 Java API
Java 8新特性之Lambda表达式与Stream API
【4月更文挑战第16天】本文将介绍Java 8中的两个重要新特性:Lambda表达式和Stream API。Lambda表达式是Java 8中引入的一种新的编程语法,它允许我们将函数作为参数传递给其他方法,从而使代码更加简洁、易读。Stream API是Java 8中引入的一种新的数据处理方式,它允许我们以声明式的方式处理数据,从而使代码更加简洁、高效。本文将通过实例代码详细讲解这两个新特性的使用方法和优势。
|
18天前
|
前端开发 Oracle Java
Java 22 新增利器: 使用 Java Stream Gather 优雅地处理流中的状态
Java 22 新增利器: 使用 Java Stream Gather 优雅地处理流中的状态
25 0
|
18天前
|
存储 Java 关系型数据库
掌握Java 8 Stream API的艺术:详解流式编程(一)
掌握Java 8 Stream API的艺术:详解流式编程
49 1
|
19天前
|
Java
Java8的stream流中flatMap()方法的作用
Java8的stream流中flatMap()方法的作用
24 10
|
21天前
|
存储 Java API
java8新特性 lambda表达式、Stream、Optional
java8新特性 lambda表达式、Stream、Optional
|
1月前
|
NoSQL Java 关系型数据库
Java 8 更新的新特性 (函数式接口 lambda stream option)
Java 8 更新的新特性 (函数式接口 lambda stream option)
55 0