0

0

Java Stream API:多表关联数据聚合与排序进阶实践

碧海醫心

碧海醫心

发布时间:2025-09-13 16:32:01

|

727人浏览过

|

来源于php中文网

原创

Java Stream API:多表关联数据聚合与排序进阶实践

本文深入探讨如何利用Java Stream API高效处理多表关联数据,实现复杂的数据聚合、筛选和排序逻辑。我们将详细演示如何从电影评分数据中找出平均分最高的N部电影,并根据预算进行二次排序,全面展现Stream API在内存数据处理中的强大能力和灵活实践。

背景与挑战

在实际应用中,我们经常需要处理来自不同来源但逻辑关联的数据。例如,给定用户、电影和评分三张表的数据,我们可能需要找出平均评分最高的电影,并在此基础上根据其他属性(如预算)进行排序。传统的关系型数据库可以通过复杂的sql查询轻松实现这一目标。然而,当数据已经在内存中以对象集合的形式存在时,如何利用java的强大功能,尤其是stream api,以声明式、高效的方式完成此类复杂的数据操作,是许多开发者面临的挑战。

本教程将以一个具体场景为例:从电影评分数据中,首先找出平均分最高的5部电影,然后将这5部电影按照预算从高到低进行排序。

数据模型构建

为了模拟实际场景,我们首先定义所需的数据模型。Java 16及更高版本引入的record类型非常适合这种不可变数据载体的定义,它简洁且功能强大。

// 电影评分记录
record Score(int userId, int movieId, int score) {}

// 电影信息记录
record Movie(int id, String name, int budget) {}

接下来,我们创建一些示例数据来演示:

List movies = List.of(
    new Movie(101, "Mov 1", 200),
    new Movie(102, "Mov 2", 500),
    new Movie(103, "Mov 3", 300),
    new Movie(104, "Mov 4", 450),
    new Movie(105, "Mov 5", 600),
    new Movie(106, "Mov 6", 150)
);

List scores = List.of(
    new Score(1, 101, 7),
    new Score(2, 101, 8),
    new Score(1, 102, 6),
    new Score(2, 102, 9),
    new Score(1, 103, 8),
    new Score(2, 103, 7),
    new Score(1, 104, 9),
    new Score(2, 104, 8),
    new Score(1, 105, 7),
    new Score(2, 105, 7),
    new Score(1, 106, 5),
    new Score(2, 106, 6)
);

核心逻辑:Java Stream 实现步骤

我们将通过一系列Stream操作来逐步实现目标。

立即学习Java免费学习笔记(深入)”;

步骤1:数据准备与辅助映射

为了高效地将电影ID映射回完整的Movie对象,我们首先创建一个Map,以电影ID为键,Movie对象为值。这可以避免在后续Stream操作中重复查找或迭代整个电影列表。

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

Map movieMap = movies.stream()
    .collect(Collectors.toMap(Movie::id, Function.identity()));

步骤2:计算电影平均分

接下来,我们需要计算每部电影的平均分。这可以通过scores列表的Stream操作,结合Collectors.groupingBy和Collectors.averagingDouble来实现。groupingBy将所有评分按movieId分组,而averagingDouble则计算每个组的平均分。

import java.util.Map.Entry;
import java.util.Collections;

// ... (previous code)

Map movieAverageScores = scores.stream()
    .collect(Collectors.groupingBy(
        Score::movieId,
        Collectors.averagingDouble(Score::score)
    ));

此时,movieAverageScores将是一个Map,其中键是movieId,值是对应的平均分。

步骤3:筛选平均分最高的电影

从平均分Map中,我们需要找出平均分最高的5部电影。这需要将Map的entrySet()转换为Stream,然后进行排序和限制。

Cutout.Pro抠图
Cutout.Pro抠图

AI批量抠图去背景

下载
// ... (previous code)

List top5MoviesByScore = movieAverageScores.entrySet().stream()
    // 按平均分降序排序
    .sorted(Collections.reverseOrder(Entry.comparingByValue()))
    // 限制为前5个
    .limit(5)
    // 将Map.Entry的Key(movieId)映射回Movie对象
    .map(e -> movieMap.get(e.getKey()))
    .toList(); // 收集为List,此时已经获取了前5部电影,但尚未按预算排序

注意:movieMap.get(e.getKey())这里假设所有评分中的movieId都能在movies列表中找到。在实际应用中,可能需要增加空值检查。

步骤4:关联电影信息并二次排序

现在我们已经得到了平均分最高的5部电影(top5MoviesByScore),但它们还未按预算排序。我们需要对这个列表进行第二次排序,这次是根据电影的budget字段进行降序排列

import java.util.Comparator;

// ... (previous code)

List finalTop5SortedByBudget = top5MoviesByScore.stream()
    // 按预算降序排序
    .sorted(Collections.reverseOrder(Comparator.comparing(Movie::budget)))
    .toList();

或者,我们可以将步骤3和步骤4合并,形成一个更长的链式操作:

// ... (previous code)

List finalTop5SortedByBudget = movieAverageScores.entrySet().stream()
    .sorted(Collections.reverseOrder(Entry.comparingByValue())) // 1. 按平均分降序
    .limit(5) // 2. 取前5个
    .map(e -> movieMap.get(e.getKey())) // 3. 映射到Movie对象
    .sorted(Collections.reverseOrder(Comparator.comparing(Movie::budget))) // 4. 按预算降序
    .toList(); // 5. 收集结果

完整示例代码

将以上所有步骤整合到一个可运行的main方法中:

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;

public class MovieAnalysis {

    // 电影评分记录
    record Score(int userId, int movieId, int score) {}

    // 电影信息记录
    record Movie(int id, String name, int budget) {}

    public static void main(String[] args) {
        // 示例电影数据
        List movies = List.of(
            new Movie(101, "Mov 1", 200),
            new Movie(102, "Mov 2", 500),
            new Movie(103, "Mov 3", 300),
            new Movie(104, "Mov 4", 450),
            new Movie(105, "Mov 5", 600),
            new Movie(106, "Mov 6", 150)
        );

        // 示例评分数据
        List scores = List.of(
            new Score(1, 101, 7),
            new Score(2, 101, 8), // Avg 101: 7.5
            new Score(1, 102, 6),
            new Score(2, 102, 9), // Avg 102: 7.5
            new Score(1, 103, 8),
            new Score(2, 103, 7), // Avg 103: 7.5
            new Score(1, 104, 9),
            new Score(2, 104, 8), // Avg 104: 8.5
            new Score(1, 105, 7),
            new Score(2, 105, 7), // Avg 105: 7.0
            new Score(1, 106, 5),
            new Score(2, 106, 6)  // Avg 106: 5.5
        );

        // 步骤1: 创建电影ID到电影对象的映射,用于高效查找
        Map movieMap = movies.stream()
            .collect(Collectors.toMap(Movie::id, Function.identity()));

        // 步骤2-5: 综合Stream操作
        List top5MoviesSortedByBudget = scores.stream()
            // 2. 按movieId分组并计算平均分
            .collect(Collectors.groupingBy(
                Score::movieId,
                Collectors.averagingDouble(Score::score)))
            // 将Map的EntrySet转换为Stream,以便排序和筛选
            .entrySet().stream()
            // 3. 按平均分降序排序
            .sorted(Collections.reverseOrder(Entry.comparingByValue()))
            // 3. 限制为前5个
            .limit(5)
            // 4. 将movieId映射回Movie对象
            .map(e -> movieMap.get(e.getKey()))
            // 4. 对这前5部电影按预算降序排序
            .sorted(Collections.reverseOrder(Comparator.comparing(Movie::budget)))
            // 5. 收集结果
            .toList();

        // 打印结果
        System.out.println("平均分最高且按预算排序的前5部电影:");
        top5MoviesSortedByBudget.forEach(System.out::println);
    }
}

运行输出示例: 根据示例数据,电影的平均分如下: Mov 1 (101): 7.5 Mov 2 (102): 7.5 Mov 3 (103): 7.5 Mov 4 (104): 8.5 Mov 5 (105): 7.0 Mov 6 (106): 5.5

平均分最高的前5部电影(以及它们的预算):

  1. Mov 4 (104): 8.5分, 预算 450
  2. Mov 1 (101): 7.5分, 预算 200
  3. Mov 2 (102): 7.5分, 预算 500
  4. Mov 3 (103): 7.5分, 预算 300
  5. Mov 5 (105): 7.0分, 预算 600

当平均分相同时,Stream的sorted操作可能会保持原有顺序(取决于具体实现),但我们最终会根据预算进行二次排序。 按照预算降序排序后: Movie[id=105, name=Mov 5, budget=600] (平均分 7.0) Movie[id=102, name=Mov 2, budget=500] (平均分 7.5) Movie[id=104, name=Mov 4, budget=450] (平均分 8.5) Movie[id=103, name=Mov 3, budget=300] (平均分 7.5) Movie[id=101, name=Mov 1, budget=200] (平均分 7.5)

注意:在平均分相同的情况下,limit(5)会选择哪些电影,这可能取决于它们在entrySet()中的迭代顺序。如果需要更严格的同分处理规则(例如,同分时按预算降序,然后再取前5),则需要调整排序逻辑,在第一次排序时就考虑预算。不过,当前的需求是“先取平均分最高的5部,再对这5部按预算排序”,所以上述实现是符合的。

注意事项与最佳实践

  • 数据量考量: Java Stream API主要适用于内存中的数据处理。对于超大规模数据(例如,数亿条记录),将所有数据加载到内存中可能会导致内存溢出。在这种情况下,数据库查询(SQL)或大数据处理框架(如Spark)是更合适的选择。
  • 性能优化: 创建movieMap是一个重要的优化点。如果没有这个映射,每次需要Movie对象时都去遍历movies列表,会大大降低性能。
  • 空值处理: 在map(e -> movieMap.get(e.getKey()))这一步,如果scores中存在movieId但在movies列表中不存在的情况,movieMap.get()将返回null。这可能导致NullPointerException。在生产代码中,应考虑Optional或过滤掉这些无效的条目。
  • 可读性: 尽管Stream API支持链式调用,但过长的链式操作有时会降低代码的可读性。适当地拆分成多个变量或方法,可以提高代码的清晰度。
  • record的优势: record类型自动提供了equals(), hashCode(), toString()以及构造函数和访问器方法,极大地简化了数据模型的定义,并使其不可变,减少了潜在的错误。

总结

通过本教程,我们深入探讨了如何利用Java Stream API处理多表关联数据,并实现了复杂的数据聚合、筛选和二次排序逻辑。从数据模型构建、计算平均分、筛选Top N,到最终的二次排序,Java Stream API提供了一种强大、声明式且高效的方式来操作内存中的数据集合。掌握这些技巧,将有助于开发者更灵活、更优雅地应对各种复杂的数据处理需求。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

751

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

328

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

350

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1304

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

361

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

881

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

581

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

425

2024.04.29

2026赚钱平台入口大全
2026赚钱平台入口大全

2026年最新赚钱平台入口汇总,涵盖任务众包、内容创作、电商运营、技能变现等多类正规渠道,助你轻松开启副业增收之路。阅读专题下面的文章了解更多详细内容。

54

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3.1万人学习

C# 教程
C# 教程

共94课时 | 8.1万人学习

Java 教程
Java 教程

共578课时 | 54.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号