
在实际应用中,我们经常需要处理来自不同来源但逻辑关联的数据。例如,给定用户、电影和评分三张表的数据,我们可能需要找出平均评分最高的电影,并在此基础上根据其他属性(如预算)进行排序。传统的关系型数据库可以通过复杂的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<Movie> 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<Score> 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)
);我们将通过一系列Stream操作来逐步实现目标。
立即学习“Java免费学习笔记(深入)”;
为了高效地将电影ID映射回完整的Movie对象,我们首先创建一个Map<Integer, Movie>,以电影ID为键,Movie对象为值。这可以避免在后续Stream操作中重复查找或迭代整个电影列表。
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
Map<Integer, Movie> movieMap = movies.stream()
.collect(Collectors.toMap(Movie::id, Function.identity()));接下来,我们需要计算每部电影的平均分。这可以通过scores列表的Stream操作,结合Collectors.groupingBy和Collectors.averagingDouble来实现。groupingBy将所有评分按movieId分组,而averagingDouble则计算每个组的平均分。
import java.util.Map.Entry;
import java.util.Collections;
// ... (previous code)
Map<Integer, Double> movieAverageScores = scores.stream()
.collect(Collectors.groupingBy(
Score::movieId,
Collectors.averagingDouble(Score::score)
));此时,movieAverageScores将是一个Map<Integer, Double>,其中键是movieId,值是对应的平均分。
从平均分Map中,我们需要找出平均分最高的5部电影。这需要将Map的entrySet()转换为Stream,然后进行排序和限制。
// ... (previous code)
List<Movie> 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列表中找到。在实际应用中,可能需要增加空值检查。
现在我们已经得到了平均分最高的5部电影(top5MoviesByScore),但它们还未按预算排序。我们需要对这个列表进行第二次排序,这次是根据电影的budget字段进行降序排列。
import java.util.Comparator;
// ... (previous code)
List<Movie> finalTop5SortedByBudget = top5MoviesByScore.stream()
// 按预算降序排序
.sorted(Collections.reverseOrder(Comparator.comparing(Movie::budget)))
.toList();或者,我们可以将步骤3和步骤4合并,形成一个更长的链式操作:
// ... (previous code)
List<Movie> 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<Movie> 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<Score> 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<Integer, Movie> movieMap = movies.stream()
.collect(Collectors.toMap(Movie::id, Function.identity()));
// 步骤2-5: 综合Stream操作
List<Movie> 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部电影(以及它们的预算):
当平均分相同时,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处理多表关联数据,并实现了复杂的数据聚合、筛选和二次排序逻辑。从数据模型构建、计算平均分、筛选Top N,到最终的二次排序,Java Stream API提供了一种强大、声明式且高效的方式来操作内存中的数据集合。掌握这些技巧,将有助于开发者更灵活、更优雅地应对各种复杂的数据处理需求。
以上就是Java Stream API:多表关联数据聚合与排序进阶实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号