第07章:flatMap():处理嵌套数据的利器

比如有一个新需求:"我们需要统计所有班级中学生的总数,还要找出所有学生中成绩最高的。"数据结构是这样的:每个学校有多个班级,每个班级有多个学生。用传统方式处理这种嵌套结构:

java 复制代码
int totalStudents = 0;
for (School school : schools) {
    for (Class clazz : school.getClasses()) {
        totalStudents += clazz.getStudents().size();
    }
}

看着这层层嵌套的循环,感觉:"这代码写得像俄罗斯套娃一样..."。这时候想起:"用flatMap()啊!专门用来'拍扁'嵌套结构,一行代码就能搞定。"

java 复制代码
int totalStudents = schools.stream()
        .flatMap(school -> school.getClasses().stream())
        .flatMap(clazz -> clazz.getStudents().stream())
        .mapToInt(student -> 1)
        .sum();

今天我们就来学习flatMap(),这个处理嵌套数据的神器,看它如何把复杂的嵌套结构"拍扁"成简单的流!

flatMap()基础:拍扁嵌套结构

flatMap()与map()的区别

map()是一对一转换,而flatMap()是一对多转换,它会将每个元素转换为一个流,然后将所有流合并成一个流:

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapBasics {
    public static void main(String[] args) {
        System.out.println("=== flatMap()与map()的区别 ===");

        List<String> sentences = Arrays.asList(
                "Hello World",
                "Java Stream API",
                "Functional Programming"
        );

        // 使用map():每个句子转换为单词数组
        System.out.println("使用map():");
        List<String[]> wordArrays = sentences.stream()
                .map(sentence -> sentence.split(" "))  // 每个句子变成String[]
                .collect(Collectors.toList());

        System.out.println("结果类型: List<String[]>");
        wordArrays.forEach(arr -> System.out.println("数组: " + Arrays.toString(arr)));

        // 使用flatMap():将所有单词"拍扁"成一个流
        System.out.println("\n使用flatMap():");
        List<String> allWords = sentences.stream()
                .flatMap(sentence -> Arrays.stream(sentence.split(" ")))  // 拍扁成单词流
                .collect(Collectors.toList());

        System.out.println("结果类型: List<String>");
        System.out.println("所有单词: " + allWords);

        // 更直观的对比
        System.out.println("\n处理过程对比:");
        System.out.println("原始数据: " + sentences);
        System.out.println("map()结果: 3个数组 [每个句子一个数组]");
        System.out.println("flatMap()结果: " + allWords.size() + "个单词 [所有单词在一个列表中]");
    }
}

输出结果:

makefile 复制代码
=== flatMap()与map()的区别 ===
使用map():
结果类型: List<String[]>
数组: [Hello, World]
数组: [Java, Stream, API]
数组: [Functional, Programming]

使用flatMap():
结果类型: List<String>
所有单词: [Hello, World, Java, Stream, API, Functional, Programming]

处理过程对比:
原始数据: [Hello World, Java Stream API, Functional Programming]
map()结果: 3个数组 [每个句子一个数组]
flatMap()结果: 7个单词 [所有单词在一个列表中]

💡 关键理解

  • map():1个输入 → 1个输出
  • flatMap():1个输入 → 多个输出(然后拍扁成一个流)

数字集合的拍扁处理

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapNumbers {
    public static void main(String[] args) {
        System.out.println("=== 数字集合的拍扁处理 ===");

        List<List<Integer>> nestedNumbers = Arrays.asList(
                Arrays.asList(1, 2, 3),
                Arrays.asList(4, 5),
                Arrays.asList(6, 7, 8, 9)
        );

        System.out.println("嵌套数字列表: " + nestedNumbers);

        // 使用flatMap()拍扁
        List<Integer> flatNumbers = nestedNumbers.stream()
                .flatMap(List::stream)  // 将每个List<Integer>转换为流,然后合并
                .collect(Collectors.toList());

        System.out.println("拍扁后: " + flatNumbers);

        // 进一步处理:找出所有偶数
        List<Integer> evenNumbers = nestedNumbers.stream()
                .flatMap(List::stream)          // 先拍扁
                .filter(n -> n % 2 == 0)        // 再过滤偶数
                .collect(Collectors.toList());

        System.out.println("所有偶数: " + evenNumbers);

        // 计算所有数字的总和
        int sum = nestedNumbers.stream()
                .flatMap(List::stream)
                .mapToInt(Integer::intValue)
                .sum();

        System.out.println("数字总和: " + sum);
    }
}

输出结果:

ini 复制代码
=== 数字集合的拍扁处理 ===
嵌套数字列表: [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
拍扁后: [1, 2, 3, 4, 5, 6, 7, 8, 9]
所有偶数: [2, 4, 6, 8]
数字总和: 45

处理复杂对象的嵌套结构

学校-班级-学生的层级处理

让我们用一个实际的业务场景来演示flatMap()的威力:

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class NestedObjectProcessing {
    public static void main(String[] args) {
        System.out.println("=== 学校班级学生数据处理 ===");

        // 构造测试数据
        List<School> schools = createSchoolData();

        // 需求1: 统计所有学生总数
        long totalStudents = schools.stream()
                .flatMap(school -> school.getClasses().stream())    // 拍扁班级
                .flatMap(clazz -> clazz.getStudents().stream())     // 拍扁学生
                .count();

        System.out.println("所有学生总数: " + totalStudents);

        // 需求2: 找出所有学生姓名
        List<String> allStudentNames = schools.stream()
                .flatMap(school -> school.getClasses().stream())
                .flatMap(clazz -> clazz.getStudents().stream())
                .map(Student::getName)  // 提取姓名
                .collect(Collectors.toList());

        System.out.println("所有学生姓名: " + allStudentNames);

        // 需求3: 找出所有优秀学生(成绩>85)
        List<Student> excellentStudents = schools.stream()
                .flatMap(school -> school.getClasses().stream())
                .flatMap(clazz -> clazz.getStudents().stream())
                .filter(student -> student.getScore() > 85)     // 过滤优秀学生
                .collect(Collectors.toList());

        System.out.println("\n优秀学生(>85分):");
        excellentStudents.forEach(System.out::println);

        // 需求4: 按学校统计学生数量
        System.out.println("\n各学校学生数量:");
        schools.forEach(school -> {
            long count = school.getClasses().stream()
                    .flatMap(clazz -> clazz.getStudents().stream())
                    .count();
            System.out.println(school.getName() + ": " + count + "人");
        });

        // 需求5: 找出成绩最高的学生
        Student topStudent = schools.stream()
                .flatMap(school -> school.getClasses().stream())
                .flatMap(clazz -> clazz.getStudents().stream())
                .max((s1, s2) -> Integer.compare(s1.getScore(), s2.getScore()))
                .orElse(null);

        if (topStudent != null) {
            System.out.println("\n成绩最高的学生: " + topStudent);
        }
    }

    private static List<School> createSchoolData() {
        // 第一小学
        List<Student> class1A = Arrays.asList(
                new Student("张三", 88),
                new Student("李四", 92),
                new Student("王五", 79)
        );
        List<Student> class1B = Arrays.asList(
                new Student("赵六", 85),
                new Student("孙七", 90)
        );

        School school1 = new School("第一小学", Arrays.asList(
                new Class("一年级A班", class1A),
                new Class("一年级B班", class1B)
        ));

        // 第二小学
        List<Student> class2A = Arrays.asList(
                new Student("周八", 94),
                new Student("吴九", 87),
                new Student("郑十", 82)
        );

        School school2 = new School("第二小学", Arrays.asList(
                new Class("一年级A班", class2A)
        ));

        return Arrays.asList(school1, school2);
    }
}

class School {
    private String name;
    private List<Class> classes;

    public School(String name, List<Class> classes) {
        this.name = name;
        this.classes = classes;
    }

    public String getName() { return name; }
    public List<Class> getClasses() { return classes; }
}

class Class {
    private String name;
    private List<Student> students;

    public Class(String name, List<Student> students) {
        this.name = name;
        this.students = students;
    }

    public String getName() { return name; }
    public List<Student> getStudents() { return students; }
}

class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() { return name; }
    public int getScore() { return score; }

    @Override
    public String toString() {
        return String.format("%s(分数:%d)", name, score);
    }
}

输出结果:

makefile 复制代码
=== 学校班级学生数据处理 ===
所有学生总数: 8
所有学生姓名: [张三, 李四, 王五, 赵六, 孙七, 周八, 吴九, 郑十]

优秀学生(>85分):
张三(分数:88)
李四(分数:92)
孙七(分数:90)
周八(分数:94)
吴九(分数:87)

各学校学生数量:
第一小学: 5人
第二小学: 3人

成绩最高的学生: 周八(分数:94)

flatMap()的高级应用

处理Optional和空值

flatMap()在处理可能为空的集合时特别有用:

java 复制代码
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class FlatMapOptional {
    public static void main(String[] args) {
        System.out.println("=== flatMap()处理Optional和空值 ===");

        List<Department> departments = Arrays.asList(
                new Department("开发部", Arrays.asList("张三", "李四", "王五")),
                new Department("测试部", Collections.emptyList()),  // 空列表
                new Department("产品部", Arrays.asList("赵六", "孙七")),
                new Department("设计部", null)  // null列表
        );

        // 安全地获取所有员工姓名
        List<String> allEmployees = departments.stream()
                .flatMap(dept -> {
                    List<String> employees = dept.getEmployees();
                    // 安全处理null和空集合
                    return employees == null ?
                            java.util.stream.Stream.empty() :
                            employees.stream();
                })
                .collect(Collectors.toList());

        System.out.println("所有员工: " + allEmployees);

        // 使用Optional更优雅地处理
        List<String> allEmployeesOptional = departments.stream()
                .flatMap(dept -> Optional.ofNullable(dept.getEmployees())
                        .orElse(Collections.emptyList())
                        .stream())
                .collect(Collectors.toList());

        System.out.println("使用Optional处理: " + allEmployeesOptional);

        // 统计非空部门数量
        long nonEmptyDepts = departments.stream()
                .flatMap(dept -> {
                    List<String> employees = dept.getEmployees();
                    return employees != null && !employees.isEmpty() ?
                            java.util.stream.Stream.of(dept) :
                            java.util.stream.Stream.empty();
                })
                .count();

        System.out.println("有员工的部门数量: " + nonEmptyDepts);
    }
}

class Department {
    private String name;
    private List<String> employees;

    public Department(String name, List<String> employees) {
        this.name = name;
        this.employees = employees;
    }

    public String getName() { return name; }
    public List<String> getEmployees() { return employees; }
}

输出结果:

makefile 复制代码
=== flatMap()处理Optional和空值 ===
所有员工: [张三, 李四, 王五, 赵六, 孙七]
使用Optional处理: [张三, 李四, 王五, 赵六, 孙七]
有员工的部门数量: 2

字符串处理的高级应用

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StringFlatMapAdvanced {
    public static void main(String[] args) {
        System.out.println("=== 字符串处理的高级应用 ===");

        List<String> documents = Arrays.asList(
                "Java 8 Stream API",
                "Lambda Expressions",
                "Functional Programming"
        );

        // 1. 获取所有不重复的字符
        List<Character> uniqueChars = documents.stream()
                .flatMap(doc -> doc.chars()                    // 转为IntStream
                        .filter(c -> c != ' ')       // 过滤空格
                        .mapToObj(c -> (char) c))    // 转为Character流
                .distinct()                                    // 去重
                .sorted()                                      // 排序
                .collect(Collectors.toList());

        System.out.println("所有不重复字符: " + uniqueChars);

        // 2. 统计单词频率(简化版)
        List<String> allWords = documents.stream()
                .flatMap(doc -> Arrays.stream(doc.toLowerCase().split("\s+")))
                .collect(Collectors.toList());

        System.out.println("所有单词: " + allWords);

        // 3. 查找包含特定字符的单词
        String targetChar = "a";
        List<String> wordsWithA = documents.stream()
                .flatMap(doc -> Arrays.stream(doc.split("\s+")))
                .filter(word -> word.toLowerCase().contains(targetChar))
                .distinct()
                .collect(Collectors.toList());

        System.out.println("包含字母'" + targetChar + "'的单词: " + wordsWithA);

        // 4. 处理CSV数据
        List<String> csvLines = Arrays.asList(
                "张三,李四,王五",
                "赵六,孙七",
                "周八,吴九,郑十,钱一"
        );

        List<String> allNames = csvLines.stream()
                .flatMap(line -> Arrays.stream(line.split(",")))
                .map(String::trim)  // 清除可能的空格
                .collect(Collectors.toList());

        System.out.println("CSV中所有姓名: " + allNames);
    }
}

输出结果:

less 复制代码
=== 字符串处理的高级应用 ===
所有不重复字符: [8, A, E, F, I, J, L, P, S, a, b, c, d, e, g, i, l, m, n, o, p, r, s, t, u, v, x]
所有单词: [java, 8, stream, api, lambda, expressions, functional, programming]
包含字母'a'的单词: [Java, Stream, API, Lambda, Functional, Programming]
CSV中所有姓名: [张三, 李四, 王五, 赵六, 孙七, 周八, 吴九, 郑十, 钱一]

性能考虑和最佳实践

避免不必要的中间集合创建

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapPerformance {
    public static void main(String[] args) {
        System.out.println("=== flatMap()性能优化 ===");

        List<String> sentences = Arrays.asList(
                "Hello World Java",
                "Stream API Programming",
                "Functional Style Coding"
        );

        // ❌ 性能较差:创建了中间数组
        System.out.println("方式1: 创建中间数组");
        List<String> words1 = sentences.stream()
                .map(s -> s.split(" "))              // 创建String[]数组
                .flatMap(Arrays::stream)             // 再拍扁
                .collect(Collectors.toList());
        System.out.println("结果: " + words1);

        // ✅ 性能更好:直接使用流
        System.out.println("\n方式2: 直接流式处理");
        List<String> words2 = sentences.stream()
                .flatMap(s -> Arrays.stream(s.split(" ")))  // 直接拍扁
                .collect(Collectors.toList());
        System.out.println("结果: " + words2);

        // 复杂场景下的性能对比
        System.out.println("\n性能对比场景:");
        long startTime = System.nanoTime();

        sentences.stream()
                .flatMap(s -> Arrays.stream(s.split(" ")))
                .filter(word -> word.length() > 4)
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        long endTime = System.nanoTime();
        System.out.println("优化后耗时: " + (endTime - startTime) + "ns");
    }
}

输出结果:

makefile 复制代码
=== flatMap()性能优化 ===
方式1: 创建中间数组
结果: [Hello, World, Java, Stream, API, Programming, Functional, Style, Coding]

方式2: 直接流式处理
结果: [Hello, World, Java, Stream, API, Programming, Functional, Style, Coding]

性能对比场景:
优化后耗时: 1275625ns

flatMap()与其他操作的最佳组合顺序

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapOptimization {
    public static void main(String[] args) {
        System.out.println("=== 操作顺序优化 ===");

        List<List<Integer>> nestedNumbers = Arrays.asList(
                Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
                Arrays.asList(11, 12, 13, 14, 15, 16, 17, 18, 19, 20),
                Arrays.asList(21, 22, 23, 24, 25, 26, 27, 28, 29, 30)
        );

        // 需求:找出所有大于15的偶数

        // ❌ 效率较低:先拍扁所有数据,再过滤
        System.out.println("方式1: 先flatMap再filter");
        List<Integer> result1 = nestedNumbers.stream()
                .flatMap(List::stream)           // 拍扁所有30个数字
                .filter(n -> n > 15)             // 过滤大于15的
                .filter(n -> n % 2 == 0)         // 过滤偶数
                .collect(Collectors.toList());
        System.out.println("结果: " + result1);

        // ✅ 效率更高:在每个子列表内先过滤,减少拍扁后的数据量
        System.out.println("\n方式2: 先filter再flatMap");
        List<Integer> result2 = nestedNumbers.stream()
                .flatMap(subList -> subList.stream()
                        .filter(n -> n > 15)     // 在子列表内先过滤
                        .filter(n -> n % 2 == 0)) // 减少需要拍扁的数据
                .collect(Collectors.toList());
        System.out.println("结果: " + result2);

        System.out.println("\n💡 在flatMap内部先进行过滤可以减少数据处理量!");
    }
}

输出结果:

makefile 复制代码
=== 操作顺序优化 ===
方式1: 先flatMap再filter
结果: [16, 18, 20, 22, 24, 26, 28, 30]

方式2: 先filter再flatMap
结果: [16, 18, 20, 22, 24, 26, 28, 30]

💡 在flatMap内部先进行过滤可以减少数据处理量!

实战案例:文档关键词分析系统

让我们构建一个文档分析系统,展示flatMap()在实际项目中的应用:

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class DocumentAnalysisSystem {
    public static void main(String[] args) {
        System.out.println("=== 文档关键词分析系统 ===");

        List<Document> documents = Arrays.asList(
                new Document("Java教程", Arrays.asList(
                        "Java是一门面向对象的编程语言",
                        "Java具有跨平台特性",
                        "Java广泛应用于企业开发"
                )),
                new Document("Stream API", Arrays.asList(
                        "Stream API是Java 8的重要特性",
                        "Stream提供了函数式编程能力",
                        "使用Stream可以简化集合操作"
                )),
                new Document("大数据处理", Arrays.asList(
                        "大数据处理需要高效的算法",
                        "Java在大数据领域有广泛应用",
                        "Stream API适合处理大量数据"
                ))
        );

        DocumentAnalyzer analyzer = new DocumentAnalyzer();

        // 1. 提取所有文档的所有单词
        List<String> allWords = analyzer.extractAllWords(documents);
        System.out.println("文档总词数: " + allWords.size());

        // 2. 统计词频
        Map<String, Long> wordFrequency = analyzer.analyzeWordFrequency(documents);
        System.out.println("\n高频词汇(出现2次以上):");
        wordFrequency.entrySet().stream()
                .filter(entry -> entry.getValue() > 1)
                .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
                .forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue() + "次"));

        // 3. 查找包含特定关键词的文档
        String keyword = "Java";
        List<String> docsWithKeyword = analyzer.findDocumentsContaining(documents, keyword);
        System.out.println("\n包含'" + keyword + "'的文档:");
        docsWithKeyword.forEach(System.out::println);

        // 4. 生成文档摘要
        List<String> summaries = analyzer.generateDocumentSummaries(documents);
        System.out.println("\n文档摘要:");
        summaries.forEach(System.out::println);
    }
}

class DocumentAnalyzer {

    // 提取所有单词
    public List<String> extractAllWords(List<Document> documents) {
        return documents.stream()
                .flatMap(doc -> doc.getContents().stream())        // 拍扁所有段落
                .flatMap(paragraph -> Arrays.stream(paragraph.split("\s+")))  // 拍扁所有单词
                .map(word -> word.replaceAll("[^\u4e00-\u9fa5a-zA-Z]", ""))  // 清理标点
                .filter(word -> !word.isEmpty())                  // 过滤空字符串
                .collect(Collectors.toList());
    }

    // 分析词频
    public Map<String, Long> analyzeWordFrequency(List<Document> documents) {
        return extractAllWords(documents).stream()
                .collect(Collectors.groupingBy(
                        Function.identity(),              // 按单词分组
                        Collectors.counting()             // 计数
                ));
    }

    // 查找包含关键词的文档
    public List<String> findDocumentsContaining(List<Document> documents, String keyword) {
        return documents.stream()
                .filter(doc -> doc.getContents().stream()
                        .anyMatch(content -> content.contains(keyword)))
                .map(Document::getTitle)
                .collect(Collectors.toList());
    }

    // 生成文档摘要
    public List<String> generateDocumentSummaries(List<Document> documents) {
        return documents.stream()
                .map(doc -> {
                    long wordCount = doc.getContents().stream()
                            .flatMap(content -> Arrays.stream(content.split("\s+")))
                            .count();

                    String firstSentence = doc.getContents().isEmpty() ?
                            "" : doc.getContents().get(0);

                    return String.format("%s [%d词] - %s",
                            doc.getTitle(), wordCount,
                            firstSentence.length() > 20 ?
                                    firstSentence.substring(0, 20) + "..." : firstSentence);
                })
                .collect(Collectors.toList());
    }
}

class Document {
    private String title;
    private List<String> contents;

    public Document(String title, List<String> contents) {
        this.title = title;
        this.contents = contents;
    }

    public String getTitle() { return title; }
    public List<String> getContents() { return contents; }
}

输出结果:

arduino 复制代码
=== 文档关键词分析系统 ===
文档总词数: 12

高频词汇(出现2次以上):
Stream: 2次

包含'Java'的文档:
Java教程
Stream API
大数据处理

文档摘要:
Java教程 [3词] - Java是一门面向对象的编程语言
Stream API [5词] - Stream API是Java 8的重要...
大数据处理 [4词] - 大数据处理需要高效的算法

本章小结

今天我们深入学习了flatMap()方法的强大功能:

核心概念:

  • 拍扁操作:将嵌套结构"拍扁"成单层流
  • 一对多转换:每个输入元素可以产生多个输出元素
  • 流合并:将多个流合并成一个流

与map()的区别:

  • map():1→1转换,保持流的结构
  • flatMap():1→多转换,改变流的结构

主要应用场景:

  • 处理嵌套集合(List<List>)
  • 处理对象的嵌套关系(如学校-班级-学生)
  • 字符串分割和处理
  • 安全处理null和空集合

性能优化要点:

  • 避免创建不必要的中间集合
  • 在flatMap()内部先进行过滤操作
  • 合理安排操作顺序

实际应用:

  • 文档内容分析和关键词提取
  • 多层级数据结构的统计分析
  • CSV/JSON等格式数据的解析
  • 复杂业务对象的数据提取

最佳实践:

  • 使用方法引用简化代码(如List::stream
  • 结合Optional处理可能为null的集合
  • 先过滤再拍扁,提升性能
  • 合理使用peek()调试复杂的flatMap()操作

下一章我们将学习《排序和去重:sorted()和distinct()让数据更整齐》,探索如何让杂乱的数据变得井井有条!


源代码地址: github.com/qianmoQ/tut...

相关推荐
3Cloudream2 分钟前
互联网大厂Java面试:从基础到微服务云原生的深度解析
java·spring·微服务·电商·技术架构·面试解析
Eiceblue27 分钟前
Java实现PDF表格转换为CSV
java·python·pdf
自由的疯1 小时前
Java RuoYi整合Magic-Api详解
java·后端·架构
老华带你飞1 小时前
校园二手书交易|基于SprinBoot+vue的校园二手书交易管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·校园二手书交易管理系统
hoho不爱喝酒2 小时前
微服务Eureka组件的介绍、安装、使用
java·微服务·eureka·架构
华仔啊2 小时前
接口卡成PPT?这9个优化技巧让系统飞起来,亲测有效!
java
华仔啊2 小时前
别再乱 new ArrayList!8 大 Java 容器选型案例,一篇看懂
java·后端
我也要当昏君2 小时前
5.2 I/O软件
java·网络·算法
阿鹿.3 小时前
docker-相关笔记
java·笔记·docker