这个作业属于哪个课程 | 福州大学-202302软件工程实践 |
---|---|
这个作业要求在哪里 | 软件工程第二次作业–文件读取 |
这个作业的目标 | 完成对世界游泳锦标赛跳水项目相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 构建之法、JUnit 4 超详细教程、码出高效_阿里巴巴Java开发手册、IDEA单元测试–详细使用步骤 |
目录
一、Gitcode项目地址
二、PSP表格
PSP Stage | Description | Estimated Time (minutes) | Actual Time (minutes) |
---|---|---|---|
Planning | Plan | 35 | 35 |
• Estimate | • Estimate how long the task will take | 30 | 30 |
Development | Development | 1400 | 1445 |
• Analysis | • Requirement analysis (including learning new technologies) | 400 | 350 |
• Design Spec | • Generate design documentation | 35 | 40 |
• Design Review | • Design review | 50 | 60 |
• Coding Standard | • Coding standards (define appropriate standards for the current development) | 30 | 30 |
• Design | • Detailed design | 55 | 65 |
• Coding | • Coding | 520 | 560 |
• Code Review | • Code review | 45 | 50 |
• Test | • Testing (self-testing, code modifications, submitting changes) | 230 | 270 |
Reporting | Reporting | 100 | 110 |
• Test Report | • Test report | 55 | 60 |
• Size Measurement | • Measure the amount of work | 30 | 30 |
• Postmortem & Process Improvement Plan | • Postmortem and propose process improvement plans | 25 | 25 |
Total | 1645 | 1875 |
三、解题思路描述
3.1、获取数据
本次作业使用助教提供的相关数据,结构如下所示:
- athletes:存储了关于跳水项目的运动员信息。
- event:存储了项目名称与项目id的对照,通过项目名称可以找到对应的项目id。
- results:文件夹下存储以项目id命名的各个项目的成绩。包括总分与每一次比赛的小分。
3.2、数据处理
通过查阅网上资料,我选择使用Gson来解析数据:Gson 是一个由 Google 提供的用于在 Java 对象和 JSON 数据之间进行转换的库。它提供了简单易用的 API,可以方便地将 Java 对象序列化为 JSON 数据,也可以将 JSON 数据反序列化为 Java 对象。
我们可以通过gson.fromJson方法来将json数据转化为已定义的Java对象。Java类对象的定义如下图所示:
3.3、问题1
(1) 读取athletes.json文件
- 首先定义文件的路径,利用BufferedReader对象读取json文件的内容并转换为字符串。
(2) 解析json文件中的数据
- 由于athletes.json文件中是Country类的对象的数组,因此先通过
gson.fromJson()
提取出country对象,转换为country数组。 - 然后依题意先按国籍进行排序,通过
Arrays.sort();
得到按国籍进行排序的country数组。再接着以LastName为次要关键字进行排序。
(3)返回解析完毕的字符串
- 将排序完毕的信息拼接成字符串并返回。
3.4、问题2
(1) 解析输入命令
- 首先读取并处理命令,解析命令result,过滤不合规的命令,构建命令对象,然后根据合法的命令对象中的比赛名称解析出相应比赛的ID。
(2) 构建路径并解析数据
- 使用得到的比赛ID去构建文件路径,通过文件路径寻找到对应的json文件中去获取决赛结果信息。然后通过
parseFinalEventResult
方法得到所需的决赛结果。
(3)构建字符串并返回
- 将决赛结果构建成符合输出要求的字符串并返回。
四、接口设计和实现过程
4.1 总体设计
Model类:根据Gson解析json数据的要求,构建相应的Java类,例如Country类、Command类、EventResult类等,并设计相应的属性字段。因为Gson 会自动将 JSON 对象中的字段名和 Java 类中的字段名进行匹配,如果匹配成功,Gson 就会将 JSON 对象中的值赋给 Java 类中的相应字段。如果 JSON 对象中的某个字段在 Java 类中没有对应的字段,那么 Gson 就会忽略这个字段。
Util类:本次作业设计了Lib类和CoreLib类,Lib类封装了大部分用来解析数据信息的函数,例如readAthletesInfo
、processResultCommand
方法等。并用来控制相应的Model类。CoreLib类提供了输出运动员信息和输出决赛结果信息的接口。
4.2 核心部分实现过程
总体流程图:
4.3 算法的关键之处
数据读取机制:
模仿Cache的缓存机制,利用局部性原理设计算法,大大节省时间开销。
异常处理:
在读取文件、解析Json数据时加入异常处理机制,提高程序的健壮性。
接口封装:
将输出关键信息的功能封装成一个模块,便于组织和维护。
使用流操作:
流操作以声明性方式处理数据集合,在需要结果时才执行,提供了更好的性能控制。
五、关键代码展示
5.1 缓存部分
private static Map<String, String> cache = new HashMap<>();// 缓存数据的Map
// 如果数据在缓存中就直接获取数据,否则先缓存数据再返回数据
public static String getCacheValue(Command command, Map<String, String> cache) throws IOException {
String cacheKey = command.getCommandType().equals("result") ? command.getEventName() : command.getCommandType();
if (cache.containsKey(cacheKey)) {// 如果缓存中有数据就直接返回
return cache.get(cacheKey);
} else {
String cacheValue;
switch (command.getCommandType()) {
case "players":
cacheValue = Lib.readAthletesInfo();
break;
case "result":
cacheValue = Lib.getFinalEventResult(command.getEventName());
break;
case "N/A":
cacheValue = "N/A\n-----\n";
break;
default:
cacheValue = "Error\n-----\n";
break;
}
cache.put(cacheKey, cacheValue);
return cacheValue;
}
}
5.2 获取运动员信息
public static String readAthletesInfo() throws IOException {
StringBuilder sb = new StringBuilder();
// 读取JSON文件的内容
try (BufferedReader reader = new BufferedReader(new FileReader(ATHLETES_FilePath))) {
String str = reader.lines().collect(Collectors.joining());
// 解析JSON数据
Country[] countries = gson.fromJson(str, Country[].class);
// 首先对国家进行排序,然后对运动员信息排序
Arrays.sort(countries, Comparator.comparing(Country::getCountryName));
String result = Arrays.stream(countries)
.flatMap(country -> sortAthletes(country).stream()
.map(athlete -> parseAthleteInfo(athlete, country.getCountryName())))
.collect(Collectors.joining());
sb.append(result);
}
return sb.toString();
}
六、性能改进
由于每次从文件中读取数据都需要耗费大量的时间,并且可能会读取相同的命令,如果每次都都执行相同的文件读写,会导致时间的巨大开销。而空间成本相对于时间成本较为低,因此便想到模仿Cache的缓存机制,当接收到命令时,先尝试从缓存中读取数据,如果缓存中没有数据,就先从文件中读取数据,并将数据缓存到Cache中,下次读取相同命令时可以直接从缓存中取出数据,大大节省了时间的开销。我经过查阅网上资料后,选择使用HashMap作为数据结构来实现缓存,原因如下:
-
HashMap具有快速查找的特性,我们可以将键作为输入的命令(cacheKey),将值作为文件中读取出来的内容(cacheValue)。
-
每次接收到命令时,可以通过命令先查找HashMap中是否有缓存相关信息,如果有的话可以直接读取,不必进行文件读写;如果没有的话便先缓存到HashMap中,再对相关文件进行读写操作。
关键代码如下所示:
private static Map<String, String> cache = new HashMap<>();// 缓存数据的Map
if (cache.containsKey(cacheKey)) {// 如果缓存中有数据就直接返回
return cache.get(cacheKey);
} else {// 否则便缓存数据
String cacheValue;
switch (command.getCommandType()) {
case "players":
cacheValue = Lib.readAthletesInfo();
break;
case "result":
cacheValue = Lib.getFinalEventResult(command.getEventName());
break;
case "N/A":
cacheValue = "N/A\n-----\n";
break;
default:
cacheValue = "Error\n-----\n";
break;
}
cache.put(cacheKey, cacheValue);
return cacheValue;
}
七、单元测试
在Java编程中,单元测试是确保代码质量和可靠性的重要环节。经过查阅网上资料,单元测试我选择使用JUnit模块,因为JUnit这一强大的单元测试框架不仅简化了测试用例的编写,还提供了便捷的测试执行机制。通过单元测试,我们能够有效地减少程序bug的产生。基于此,我构造的测试数据如下:
部分测试代码如下:
结果覆盖率如下:
代码覆盖率大部分都较高,符合预期。
八、异常处理
异常处理首先检验命令行参数输入是否符合标准,其次检查文件路径是否以txt结尾,符合要求后方可进行功能使用,代码如下所示:
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("命令行参数个数输入错误!\n");
return;
} else {
// 检查参数是否以.txt结尾
if (!args[0].endsWith(".txt") || !args[1].endsWith(".txt")) {
System.out.println("命令行参数必须是以.txt结尾的文件路径!");
return;
}
String inputFile = args[0];
String outputFile = args[1];
try {
CoreLib.writeToFile(inputFile, outputFile);
} catch (IOException e) {
System.out.println("文件不存在:" + e.getMessage());
}
}
}
九、心得体会
通过本次作业,我不仅体会到了项目性能优化的重要性,还学习到了如何合理地规划时间去完成一个项目,逐步推进去完成它。我还学习到了用缓存去读取数据,如果按照以往,我大概会是实现了从文件读取数据之后就认为大功告成了,但是想要更好的运行体验,就还需要采用优化的手段来改善我们的程序。我还学会了用git来及时地签入项目、管理项目,并且学会使用Maven来搭建项目结构、引入jar包,还学习了java中处理json数据的工具包,总的来说收获颇丰。