这个作业属于哪个课程 | 21级软件工程 |
---|---|
这个作业要求在哪里 | 软件工程第二次作业–文件读取 |
这个作业的目标 | 完成对世界游泳锦标赛跳水项目相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 工程师的能力评估与发展、源代码管理、单元测试与回归测试 |
目录
1.GitCode仓库
2.PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
• Estimate | • 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 1050 | 1195 |
• Analysis | • 需求分析 (包括学习新技术) | 90 | 80 |
• Design Spec | • 生成设计文档 | 30 | 40 |
• Design Review | • 设计复审 | 30 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 10 |
• Design | • 具体设计 | 30 | 25 |
• Coding | • 具体编码 | 700 | 800 |
• Code Review | • 代码复审 | 30 | 20 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 200 |
Reporting | 报告 | 100 | 90 |
• Test Repor | • 测试报告 | 30 | 60 |
• Size Measurement | • 计算工作量 | 20 | 10 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 50 | 20 |
合计 | 1185 | 1305 |
3.解题思路描述
3.1 Json数据分析
首先来分析一下获取到的json数据,包括三个部分。
第一部分是results包,包括了各个项目的决赛结果。其中,Heats数组中有两个元素,分别是决赛和预赛,而我们只需要决赛结果即可。决赛结果数组是三层嵌套数组,最外层根据"TotalPoints"划分,次外层根据"Dives"划分,最内层根据"JudgesScores"划分。分析数据发现,FullName、Rank、TotalPoints和DivePoints这几个数据是我们需要的。
第二部分是athletes.json,其中的数据是个双层数组,外层数组根据国家区分,内层数组根据运动员区分。分析数据发现,CountryName、PreferredLastName、PreferredFirstName、DisciplineName和Gender这几个数据是我们需要的。
第三部分是event.json,包括了比赛相关信息。
3.2 Json数据处理
我使用alibaba的fastjson2来处理json数据。
根据json数据的关键词来获取相关数据。
JSONObject resultObject = resultsArray.getJSONObject(i);
//根据json文件中的key值获取对应的value(有的value要处理,有的value直接获取即可)
String country = resultObject.getString("CountryName");
//获取每个国家的运动员信息
JSONArray athletesArray = resultObject.getJSONArray("Participations");
//遍历内层运动员数组
for (int j = 0; j < athletesArray.size(); j++) {
String fullname = athletesArray.getJSONObject(j).getString("PreferredLastName") + " " + athletesArray.getJSONObject(j).getString("PreferredFirstName");
fileWriter.write(("Full Name:" + fullname + "\n").getBytes());
String gender = (athletesArray.getJSONObject(j).getInteger("Gender") == 0 ? "Male" : "Female");
fileWriter.write(("Gender:" + gender + "\n").getBytes());
fileWriter.write(("Country:" + country + "\n").getBytes());
fileWriter.write(("-----" + "\n").getBytes());
}
3.3 传入文件地址,循环读取操作指令
这一功能调用inputTest方法来实现,inputTest方法通过逐行读取输入文件中的操作指令,并根据指令的不同调用相应的方法进行处理,最终将处理结果输出到指定的输出文件中。具体实现思路如下:
- 根据传入的输入文件路径,使用 BufferedReader 逐行读取输入文件中的操作指令,并进行处理。
- 对于每一行读取到的操作指令,首先使用 trim() 方法去除首尾空白字符,确保操作指令的格式正确。
- 如果操作指令是 “players”,则调用 outputAllAthletesInfo 方法输出所有选手信息。
- 如果操作指令以 "result " 开头,则解析指令中的比赛项目信息,判断其是否合法。
- 如果比赛项目合法,则根据比赛项目信息调用 outputEventResult 方法输出比赛结果。
- 如果操作指令不符合以上两种格式,则输出 “Error” 到输出文件,并在其后输出 “-----”,以表示错误信息结束。
- 循环处理完所有操作指令后,关闭输入文件的读取流,处理结束。
3.4 功能1:输出所有选手信息
这一功能调用outputAllAthletesInfo方法来实现,outputAllAthletesInfo方法根据缓存是否存在,选择从文件中读取或者直接使用缓存中的运动员信息,并将这些信息逐个写入输出文件中。具体实现思路如下:
- 方法会检查是否已经存在运动员信息的缓存(即 athletes 数组),如果缓存为空,则说明是首次输出所有选手信息,需要从文件中读取。
- 如果缓存不为空,则直接使用缓存中的运动员信息进行输出。
- 对于首次输出选手信息的情况,首先从 athletes.json 文件中读取数据。这个文件中的数据是一个双层数组,外层数组按国家区分,内层数组按运动员区分。
- 遍历外层国家数组,获取每个国家的名称和对应的运动员信息数组。
- 对于每个国家的运动员信息数组,遍历其中的每个运动员,获取其全名、性别和所属国家信息。
- 将每个运动员的信息写入输出文件中,包括全名、性别和所属国家,并在每个运动员信息之后输出 “-----” 表示信息结束。
- 如果缓存不为空,则直接使用缓存中的运动员信息进行输出,遍历缓存中的 athletes 数组,将每个运动员的信息写入输出文件,并在每个运动员信息之后输出 “-----” 表示信息结束。
3.5 功能2:输出决赛每个运动项目结果
这一功能调用outputEventResult方法来实现,outputEventResult方法根据缓存是否存在指定比赛项目的结果,选择从文件中读取或者直接使用缓存中的结果,并将这些结果逐个写入输出文件中。具体实现思路如下:
- 方法会检查缓存中是否已经存在指定比赛项目的结果,如果存在,则直接从缓存中获取结果并输出。
- 如果缓存中不存在指定比赛项目的结果,则说明是首次输出该比赛项目的结果,需要从文件中读取。
- 根据传入的比赛项目名称构建相应的 JSON 文件路径,从文件中读取该比赛项目的数据。
- 解析 JSON 数据,获取比赛项目的结果数组,这个数组按照不同的标准进行层级划分。
- 遍历比赛项目的结果数组,获取每个运动员的全名、排名、总分以及每次裁判打分的信息。
- 将每个运动员的信息写入输出文件中,包括全名、排名、总分和裁判打分信息,并在每个运动员信息之后输出 “-----” 表示信息结束。
- 将结果缓存起来,以便下次快速获取。
- 如果缓存中已经存在相同比赛项目的结果,则直接从缓存中获取结果并输出,避免重复读取文件和处理数据。
4.设计与实现过程
4.1 类设计
- 实体类:设计了Athlete和Result类来存放运动员和决赛结果的相关数据。
- Lib类:包括主要的逻辑处理函数,是项目的核心类。
- DWASearch类:包含main函数,是项目的入口。
- LibTest类:测试类,用于Lib类的单元测试。
4.2 函数设计
- 主函数:传入参数并检测正确性
- 逻辑函数:
- inputTest:通过逐行读取输入文件中的操作指令,并根据指令的不同调用相应的方法进行处理,最终将处理结果输出到指定的输出文件中。
- outputAllAthletesInfo:根据缓存是否存在,选择从文件中读取或者直接使用缓存中的运动员信息,并将这些信息逐个写入输出文件中。
- outputEventResult:根据缓存是否存在指定比赛项目的结果,选择从文件中读取或者直接使用缓存中的结果,并将这些结果逐个写入输出文件中。
- 工具函数:
- isValidEvent:用于检查比赛项目名称是否合法。
- getStrFromData:用于将指定的 JSON 文件读取并转换为字符串返回。
//传入读入和读出文件地址,循环读取操作指令(判断是否是正确指令)
public static void inputTest(String input, String output);
//检查比赛项目名称是否合法
private static boolean isValidEvent(String gender, String distance, String discipline);
//将指定的json文件读取出来,并转为字符串返回
private static String getStrFromData(String jsonFileAddress);
//输出所有选手信息
public static void outputAllAthletesInfo(FileOutputStream fileWriter);
//输出决赛每个运动项目结果
public static void outputEventResult(String jsonName, FileOutputStream fileWriter);
5.关键代码展示
5.1 inputTest方法
public static void inputTest(String input, String output) {
try {
String inputPath = basedir + input;
String outputPath = basedir + output;
FileOutputStream fileOutputStream = new FileOutputStream(outputPath, true);
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(inputPath), StandardCharsets.UTF_8));
String command;
while ((command = br.readLine()) != null) {
command = command.trim();
if (command.equals("players")) {
outputAllAthletesInfo(fileOutputStream);
} else if (command.startsWith("result ")) {
String[] parts = command.split(" ");
if (parts.length != 4 || !isValidEvent(parts[1], parts[2], parts[3])) {
fileOutputStream.write(("N/A" + "\n").getBytes());
fileOutputStream.write(("-----" + "\n").getBytes());
} else {
// 操作指令合法,调用输出比赛结果的方法
String jsonName = parts[1] + " " + parts[2] + " " + parts[3] + ".json";
outputEventResult(jsonName, fileOutputStream);
}
} else {
fileOutputStream.write(("Error" + "\n").getBytes());
fileOutputStream.write(("-----" + "\n").getBytes());
}
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
5.2 outputEventResult方法
public static void outputEventResult(String jsonName, FileOutputStream fileWriter) {
try {
//若并非第一次输出一个运动项目结果(之前已输出过同一个),则可以直接输出缓存中的结果
if (results != null && results.containsKey(jsonName)) {
List<Result> resultList = results.get(jsonName);
for (Result result : resultList) {
fileWriter.write(("Full Name:" + result.getFullName() + "\n").getBytes());
fileWriter.write(("Rank:" + result.getRank() + "\n").getBytes());
fileWriter.write(("Score:" + result.getScore() + "\n").getBytes());
fileWriter.write(("-----" + "\n").getBytes());
}
} else {
//获取比赛的全部数据
JSONObject jsonObject = JSONObject.parseObject(getStrFromData(basedir + "data/results/" + jsonName));
//获取第一层Heats数组
JSONArray heatsArray = jsonObject.getJSONArray("Heats");
//Heats数组中有两个元素,分别是决赛和预赛
JSONObject finalObject = heatsArray.getJSONObject(0);
//获取决赛中的结果,决赛结果数组是三层嵌套数组,最外层根据"TotalPoints"划分,次外层根据"Dives"划分,最内层根据"JudgesScores"划分
JSONArray resultsArray = finalObject.getJSONArray("Results");
//存储运动项目结果的列表
List<Result> resultList = new ArrayList<>();
//获取每个选手的得分信息
for (int i = 0; i < resultsArray.size(); i++) {
JSONObject athleteObject = resultsArray.getJSONObject(i);
String fullName = athleteObject.getString("FullName");
String rank = athleteObject.getString("Rank");
String totalPoints = athleteObject.getString("TotalPoints");
//总得分中每个裁判的打分
String judgesScores = "";
JSONArray divesArray = athleteObject.getJSONArray("Dives");
for (int j = 0; j < divesArray.size(); j++) {
JSONObject divesScore = divesArray.getJSONObject(j);
String onceScore = divesScore.getString("DivePoints");
//生成裁判打分的连加字符串
if (j != divesArray.size() - 1) {
judgesScores += onceScore + "+";
} else {
judgesScores += onceScore;
}
}
//生成最终的得分字符串(例如:46.00 + 42.90 + 50.70 + 54.00 + 46.80 = 240.40)
String finalScore = judgesScores + "=" + totalPoints;
//将运动项目的结果缓存入HashMap,方便下次快速获取
Result result = new Result(fullName, rank, finalScore);
resultList.add(result);
//输出决赛的项目结果
fileWriter.write(("Full Name:" + fullName + "\n").getBytes());
fileWriter.write(("Rank:" + rank + "\n").getBytes());
fileWriter.write(("Score:" + finalScore + "\n").getBytes());
fileWriter.write(("-----" + "\n").getBytes());
}
results.put(jsonName, resultList);
}
} catch (IOException e) {
e.printStackTrace();
}
}
5.3 文件路径处理(解决jar包无法访问txt文件的问题)
public static String basedir;
static {
basedir = Paths.get("").toAbsolutePath().toString();
basedir = basedir.substring(0, basedir.indexOf("project-java") + 12);
basedir = basedir + "\\112101225\\src\\";
}
6.程序性能改进
这个程序的主要功能就是json数据的读取、处理和输出,那么主要影响程序性能的因素就是json数据的反复读取。即在短期内反复读取同一份json数据时,可以考虑使用缓存,把数据先存下来,然后短期内反复读取时,可以之间从缓存中获取所需数据。
// 运动员信息缓存数组
public static Athlete[] athletes;
// 比赛结果缓存HashMap
public static HashMap<String, List<Result>> results = new HashMap<>();
if (athletes == null) {
//初次输出,进行缓存处理
}else{
//多次输出,直接从缓存中获取数据
}
if (results != null && results.containsKey(jsonName)) {
//初次输出,进行缓存处理
}else{
//多次输出,直接从缓存中获取数据
}
7.单元测试展示
单元测试主要是对程序中的inputTest、outputAllAthletesInfo和outputEventResult方法进行测试,编写测试代码如下:
public static final String INPUT_FILE = "input.txt";
public static final String OUTPUT_FILE = "output.txt";
Lib lib = new Lib();
@Test
public void DWASearchTest() {
DWASearch.main(new String[]{INPUT_FILE, OUTPUT_FILE});
System.out.println("DWASearch方法测试完成");
}
@Test
public void inputTest() {
lib.inputTest(INPUT_FILE, OUTPUT_FILE);
System.out.println("inputTest方法测试完成");
}
@Test
public void outputAllAthletesInfo() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(OUTPUT_FILE, true);
lib.outputAllAthletesInfo(fileOutputStream);
System.out.println("outputAllAthletesInfo方法测试完成");
}
@Test
public void outputEventResult() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(OUTPUT_FILE, true);
String jsonName = "women 1m springboard.json";
lib.outputEventResult(jsonName, fileOutputStream);
System.out.println("outputEventResult方法测试完成");
}
处理结果如下:
8.异常处理说明
8.1 主函数参数校验
//判断传入的参数是否是两个
if (args.length != 2) {
System.out.println("需要同时传入输入文件和输出文件的路径");
return;
}
8.2 IO异常处理
使用try-catch语句块来进行异常处理,若有异常会抛出。
try {
//代码细节
} catch (IOException e) {
e.printStackTrace();
}
9.心路历程与收获
一开始看到作业需求,我并没有很好的思路,所以我反复阅读了很多遍作业需求,思考每个细节“是什么、为什么、怎么做”,逐渐对我所要写的程序有了个大致的概念。在实现过程中,我采用模块化的思路,将一个大问题拆解为一个个小问题,再逐一突破实现它们。不可避免的,我遇到了很多不懂的新技术和奇怪的bug,这也大大地锻炼了我的资料检索能力和debug调试能力。总体而言,我认为这次的作业我有很大的收获。