Java 8 Stream用法与常见问题和解决方式

1. 什么是 Stream API?

Stream API 是用于处理数据序列的功能,提供了一种高效、清晰和声明性的处理方式。Stream 不会存储数据,它是对数据源的高级抽象,可以进行聚合操作(如过滤、映射、排序、归约等)。Stream 的核心优势包括:

  • 支持链式调用,增强代码可读性。
  • 惰性求值特性,避免不必要的计算。
  • 并行处理能力,充分利用多核 CPU 提升性能。

2. Stream API 基础

Stream 可以通过多种方式创建,常见的有以下几种:

  • CollectionList 中获取。
  • 使用 Stream.of() 方法。
  • 使用 Arrays.stream() 处理数组。
  • 使用文件或 I/O 操作生成。
示例:
复制代码
List<String> stringList = Arrays.asList("Java", "Python", "C++", "JavaScript");

// 从集合获取 Stream
Stream<String> streamFromList = stringList.stream();

// 使用 Stream.of()
Stream<String> streamOfStrings = Stream.of("Hello", "World");

// 使用 Arrays.stream()
int[] intArray = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArray);

3. Stream API 常见操作详解

Stream API 提供了丰富的操作来处理数据序列,这些操作分为中间操作和终止操作。中间操作不会立即执行,它们是惰性的,只有在终止操作触发时才会执行。终止操作会结束流的处理并生成结果。以下是对 Stream API 常见操作的细化讲解,包括分组操作等高级功能。

3.1. 中间操作
3.1.1. filter(Predicate predicate)

filter 用于根据条件过滤流中的元素。它接受一个 Predicate 接口,该接口定义了一个条件,返回值为布尔类型。满足条件的元素会保留在流中,不满足的会被过滤掉。

示例:

复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.startsWith("A"))
                                  .collect(Collectors.toList());
// 输出 ["Alice"]
3.1.2. map(Function mapper)

map 用于将流中的每个元素转换为另一种形式。它接受一个 Function 接口,将元素逐一映射为新的值。

示例:

复制代码
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<Integer> wordLengths = words.stream()
                                 .map(String::length)
                                 .collect(Collectors.toList());
// 输出 [5, 6, 6]
3.1.3. flatMap(Function> mapper)

flatMap 用于将每个元素转换为一个流,然后将多个流合并为一个流。它适合处理嵌套的集合结构。

示例:

复制代码
List<List<String>> nestedList = Arrays.asList(
    Arrays.asList("one", "two"),
    Arrays.asList("three", "four")
);
List<String> flattenedList = nestedList.stream()
                                       .flatMap(List::stream)
                                       .collect(Collectors.toList());
// 输出 ["one", "two", "three", "four"]
3.1.4. distinct()

distinct 用于去除流中重复的元素,保留唯一值。

示例:

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> distinctNumbers = numbers.stream()
                                       .distinct()
                                       .collect(Collectors.toList());
// 输出 [1, 2, 3, 4, 5]
3.1.5. sorted()sorted(Comparator comparator)

sorted 用于对流中的元素进行排序,默认是自然顺序。通过传入 Comparator 可以进行自定义排序。

示例:

复制代码
List<String> names = Arrays.asList("John", "Jane", "Mark", "Emily");
List<String> sortedNames = names.stream()
                                .sorted()
                                .collect(Collectors.toList());
// 输出 ["Emily", "Jane", "John", "Mark"]

List<String> reverseSortedNames = names.stream()
                                       .sorted(Comparator.reverseOrder())
                                       .collect(Collectors.toList());
// 输出 ["Mark", "John", "Jane", "Emily"]
3.1.6. limit(long maxSize)skip(long n)
  • limit 截取流中前 maxSize 个元素。
  • skip 跳过流中前 n 个元素。

示例:

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> limitedNumbers = numbers.stream()
                                      .limit(3)
                                      .collect(Collectors.toList());
// 输出 [1, 2, 3]

List<Integer> skippedNumbers = numbers.stream()
                                      .skip(2)
                                      .collect(Collectors.toList());
// 输出 [3, 4, 5, 6]
3.2. 终止操作
3.2.1. collect(Collector collector)

collect 是用于将流中的元素收集到集合或其他数据结构中。常见的 Collector 工具包括 Collectors.toList()Collectors.toSet()Collectors.joining() 等。

示例:

复制代码
List<String> words = Arrays.asList("apple", "banana", "cherry");
String joinedWords = words.stream()
                          .collect(Collectors.joining(", "));
// 输出 "apple, banana, cherry"
3.2.2. forEach(Consumer action)

forEach 遍历流中的每个元素并执行给定的操作。

示例:

复制代码
List<String> items = Arrays.asList("item1", "item2", "item3");
items.stream()
     .forEach(System.out::println);
// 输出每个元素
3.2.3. count()

count 返回流中元素的数量。

示例:

复制代码
List<String> names = Arrays.asList("John", "Jane", "Mark");
long count = names.stream().count();
// 输出 3
3.2.4. reduce(BinaryOperator accumulator)

reduce 用于将流中的元素逐一组合成一个结果。

示例:

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> a + b);
sum.ifPresent(System.out::println);  // 输出 15
3.2.5. findFirst()findAny()
  • findFirst 返回流中第一个元素的 Optional
  • findAny 返回流中任意一个元素的 Optional,通常用于并行流。

示例:

复制代码
List<String> items = Arrays.asList("apple", "banana", "cherry");
Optional<String> firstItem = items.stream().findFirst();
firstItem.ifPresent(System.out::println);  // 输出 "apple"
3.2.6. allMatch(Predicate predicate)anyMatch(Predicate predicate)noneMatch(Predicate predicate)
  • allMatch 检查是否所有元素都满足给定条件。
  • anyMatch 检查是否有任一元素满足给定条件。
  • noneMatch 检查是否没有元素满足给定条件。

示例:

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0); // false
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true
3.3. 分组操作

Collectors.groupingBy() 是 Stream API 中的一个高级操作,它允许根据某个属性对元素进行分组。返回的结果是一个 Map,其中键是分组的依据,值是分组后的列表。

示例:按字符串长度分组

复制代码
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
Map<Integer, List<String>> groupedByLength = words.stream()
                                                  .collect(Collectors.groupingBy(String::length));

groupedByLength.forEach((length, wordList) -> 
    System.out.println("长度为 " + length + " 的单词: " + wordList)
);
// 输出:
// 长度为 5 的单词: [apple]
// 长度为 6 的单词: [banana, cherry]
// 长度为 4 的单词: [date]

示例:按员工部门分组

复制代码
class Employee {
    private String name;
    private String department;

    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }

    public String getDepartment() {
        return department;
    }

    public String getName() {
        return name;
    }
}

List<Employee> employees = Arrays.asList(
    new Employee("Alice", "IT"),
    new Employee("Bob", "HR"),
    new Employee("Charlie", "IT"),
    new Employee("David", "Finance")
);

Map<String, List<Employee>> employeesByDepartment = employees.stream()
                                                             .collect(Collectors.groupingBy(Employee::getDepartment));

employeesByDepartment.forEach((department, empList) -> {
    System.out.println("部门 " + department + ": " + 
        empList.stream().map(Employee::getName).collect(Collectors.joining(", ")));
});
// 输出:
// 部门 IT: Alice, Charlie
// 部门 HR: Bob
// 部门 Finance: David
多级分组

Collectors.groupingBy() 支持多级分组,这在实际项目中非常有用。例如,按部门和职位进行分组:

复制代码
Map<String, Map<String, List<Employee>>> multiLevelGrouping = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment, 
              Collectors.groupingBy(Employee::getPosition)));
// 返回嵌套 Map 结构,按部门和职位分组

通过这些详尽的用法和分组操作,开发者可以更轻松地处理复杂数据操作,编写清晰、高效、可维护的代码。

4. Stream API 高级操作

4.1. 并行流

Java 8 提供了并行流(parallelStream()),可以充分利用多核 CPU,提升数据处理的性能。并行流将流中的任务分发到多个线程中并行处理。

复制代码
List<Integer> largeList = IntStream.rangeClosed(1, 1000000)
                                   .boxed()
                                   .collect(Collectors.toList());

long start = System.currentTimeMillis();
largeList.parallelStream()
         .filter(n -> n % 2 == 0)
         .count();
long end = System.currentTimeMillis();
System.out.println("并行流处理时间: " + (end - start) + " ms");
4.2. 惰性求值

Stream 的中间操作是惰性的,意味着它们在没有终止操作的情况下不会被执行。惰性求值有助于优化性能,减少不必要的计算。

复制代码
Stream<String> lazyStream = Stream.of("one", "two", "three", "four")
                                  .filter(s -> {
                                      System.out.println("正在处理: " + s);
                                      return s.length() > 3;
                                  });

// 没有调用终止操作,filter 不会执行
System.out.println("未触发终止操作");

// 调用终止操作后,filter 才会被执行
lazyStream.forEach(System.out::println);

5. 常见问题及解决方式

在使用 Java 8 Stream API 时,开发者可能会遇到一些常见的坑和问题。理解这些问题及其解决方案有助于编写健壮的代码。以下是几个常见问题及其应对方法。

5.1. Collectors.toMap() 时 Key 重复问题

在使用 Collectors.toMap() 将流转换为 Map 时,如果流中的键重复,就会抛出 IllegalStateException,提示键重复。为了避免此问题,我们可以通过提供合并函数来处理重复的键。

问题示例:

复制代码
List<String> items = Arrays.asList("apple", "banana", "apple", "orange");
Map<String, Integer> itemMap = items.stream()
                                    .collect(Collectors.toMap(
                                        item -> item, 
                                        item -> 1
                                    ));
// 这段代码会抛出 IllegalStateException,因为 "apple" 键重复

解决方案:使用合并函数

通过提供一个合并函数来解决键重复问题,例如选择保留第一个值或累加值。

复制代码
Map<String, Integer> itemMap = items.stream()
                                    .collect(Collectors.toMap(
                                        item -> item, 
                                        item -> 1,
                                        (existingValue, newValue) -> existingValue + newValue // 合并函数
                                    ));
// 输出:{apple=2, banana=1, orange=1}

**解释:**合并函数 (existingValue, newValue) -> existingValue + newValue 表示当键重复时,将值进行累加。

5.2. NullPointerException 问题

在使用 Stream API 进行操作时,如果流中存在 null 值,操作如 map()filter() 等可能会抛出 NullPointerException。为了避免这种情况,通常需要在操作前进行空值检查。

问题示例:

复制代码
List<String> words = Arrays.asList("apple", null, "banana", "cherry");
List<Integer> wordLengths = words.stream()
                                 .map(String::length) // 如果遇到 null,会抛出 NullPointerException
                                 .collect(Collectors.toList());

解决方案:使用 filter() 过滤 null

复制代码
List<Integer> wordLengths = words.stream()
                                 .filter(Objects::nonNull) // 过滤掉 null 值
                                 .map(String::length)
                                 .collect(Collectors.toList());
// 输出 [5, 6, 6]
5.3. ConcurrentModificationException 问题

当使用 Stream API 遍历集合并在迭代时修改集合时,会抛出 ConcurrentModificationException。这通常发生在对原始集合进行迭代并修改它的情况下。

问题示例:

复制代码
List<String> names = new ArrayList<>(Arrays.asList("John", "Jane", "Mark", "Emily"));
names.stream().forEach(name -> {
    if (name.equals("Mark")) {
        names.remove(name); // 会抛出 ConcurrentModificationException
    }
});

解决方案:使用 removeIf() 方法或创建新的集合

复制代码
// 使用 removeIf()
names.removeIf(name -> name.equals("Mark")); // 安全地删除元素

// 或者,使用流创建新的集合
List<String> filteredNames = names.stream()
                                  .filter(name -> !name.equals("Mark"))
                                  .collect(Collectors.toList());
5.4. Stream 性能问题

虽然 Stream API 提供了优雅的语法,但在某些情况下会有性能问题,尤其是在使用 parallelStream() 时。并行流可以提高处理大量数据的性能,但在小数据集或 I/O 密集型操作中,可能会带来开销并导致性能下降。

建议:

  • 在使用并行流前,分析数据规模和应用场景,确定是否有必要。
  • 避免在 Stream 中进行复杂的同步操作或共享可变状态,以避免线程安全问题。

示例:

复制代码
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
                                 .boxed()
                                 .collect(Collectors.toList());

// 并行流
long start = System.currentTimeMillis();
numbers.parallelStream()
       .filter(n -> n % 2 == 0)
       .count();
long end = System.currentTimeMillis();
System.out.println("并行流处理时间: " + (end - start) + " ms");

// 顺序流
start = System.currentTimeMillis();
numbers.stream()
       .filter(n -> n % 2 == 0)
       .count();
end = System.currentTimeMillis();
System.out.println("顺序流处理时间: " + (end - start) + " ms");

**注意:**在小数据集上,并行流的性能可能不如顺序流好。

5.5. 流的短路操作

Stream API 中有一些短路操作,可以减少不必要的处理,从而提高性能。

  • findFirst()findAny():返回第一个或任意一个符合条件的元素,适用于需要快速找到结果的情况。
  • limit():截断流,适用于只需要处理部分数据时。
  • anyMatch()allMatch()noneMatch():检查流中是否有满足条件的元素,支持短路操作。

示例:使用短路操作优化性能

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
boolean hasEvenNumber = numbers.stream()
                               .anyMatch(n -> n % 2 == 0); // 一旦找到第一个偶数,就会终止遍历
System.out.println("是否存在偶数: " + hasEvenNumber); // 输出 true
5.6. 数据并行性与共享可变状态

在使用并行流时,如果流中的元素共享了可变状态,可能会导致数据不一致或线程安全问题。

问题示例:

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
       .forEach(results::add); // 可能会导致数据不一致
System.out.println(results);

解决方案:

使用线程安全的集合,如 Collections.synchronizedList()ConcurrentLinkedQueue,或者使用 collect() 收集结果。

复制代码
List<Integer> results = numbers.parallelStream()
                               .collect(Collectors.toList()); // 推荐做法

总结起来,Stream API 提供了强大的功能和简洁的语法,但在实际项目中使用时,需要了解和避免常见的陷阱和问题,以确保代码的安全性和性能。

相关推荐
困知勉行1985几秒前
springboot整合redis
java·spring boot·redis
wuk9984 分钟前
基于MATLAB实现栅格地图全覆盖移动路径规划
开发语言·matlab
颜淡慕潇5 分钟前
深度解析官方 Spring Boot 稳定版本及 JDK 配套策略
java·后端·架构
Victor3566 分钟前
Hibernate(28)Hibernate的级联操作是什么?
后端
Victor35612 分钟前
Hibernate(27)Hibernate的查询策略是什么?
后端
中年程序员一枚15 分钟前
Springboot报错Template not found For name “java/lang/Object_toString.sql
java·spring boot·python
幽络源小助理24 分钟前
PHP虚拟商品自动发卡系统源码 – 支持文章付费阅读与自动发货
开发语言·php
故事不长丨28 分钟前
C#集合:解锁高效数据管理的秘密武器
开发语言·windows·c#·wpf·集合·winfrom·字典
知识分享小能手32 分钟前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04中的Java与Android开发环境 (20)
java·学习·ubuntu
南屿欣风39 分钟前
FeignClient 踩坑:@FeignClient 同时配 value 和 url 的 “无效服务名” 问题
java