软件工程第二次作业--文件读取

222100129梅明胜 2024-03-03 09:32:00
这个作业属于哪个课程<2302软件工程>
这个作业要求在哪里<软件工程第二次作业--文件读取>
这个作业的目标<完成对**世界游泳锦标赛跳水项目**相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序>
其他参考文献《码出高效_阿里巴巴Java开发手册》单元测试和回归测试源代码管理、[IDEA单元测试](IDEA单元测试--详细使用步骤_idea 单元测试-CSDN博客)

目录

  • 0. Gitcode项目地址
  • 1. PSP表格
  • 2. 解题思路描述
  • 2.1 问题1
  • 2.1.1 JSON资源获取
  • 2.1.2 分析JSON资源结构
  • 2.1.3 解题思路
  • 2.1.4 独到之处
  • 2.2 问题2
  • 2.2.1 JSON资源获取
  • 2.2.2 分析JSON资源结构
  • 2.2.3 解题思路
  • 2.2.4 独到之处
  • 3. 接口设计和实现过程
  • 3.1 接口设计
  • 3.2 实现过程
  • 4. 关键代码展示
  • 5. 性能改进
  • 5.1 使用 HashMap 实现缓存机制
  • 5.1.1 改进效果
  • 5.2 使用BufferedWriter提高效率
  • 6. 单元测试
  • 6.1 DWASearch类测试
  • 6.2 input.txt指令内容测试
  • 6.3 提升覆盖率
  • 6.4 自动单元测试
  • 7. 异常处理
  • 7.1 指令错误
  • 7.2 IO异常
  • 8. 心得体会

0. Gitcode项目地址

[meiyuan0369 / Project Java](meiyuan0369 / Project Java · GitCode)

1. PSP表格

分钟计算:天数*分钟数

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning 计划 1*120 5*30
• Estimate • 估计这个任务需要多少时间 5*240 8*240
Development开发3*2405*240
• Analysis• 需求分析 (包括学习新技术)1*2401*240
• Design Spec• 生成设计文档1*2401*240
• Design Review• 设计复审1*603*30
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)1*301*30
• Design• 具体设计4*305*30
• Coding• 具体编码3*1805*180
• Code Review• 代码复审1*601*60
• Test• 测试(自我测试,修改代码,提交修改)1*1202*60
Reporting报告1*1201*240
• Test Repor• 测试报告1*301*60
• Size Measurement• 计算工作量1*301*30
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划1*301*30
合计36605100

2. 解题思路描述

2.1 问题1

2.1.1 JSON资源获取

  1. 点击链接查看参赛选手信息,然后在网页中鼠标右键,选择“检查”,打开开发者工具

img

  1. img

  1. img

  1. img

  1. 在VSCode等编译软件中格式化Json文件,然后下一步分析JSON资源结构

img

2.1.2 分析JSON资源结构

  1. 首先发现Json文件最外层是[ ... ]结构的数组,里面由多个{ ... }对象组成,每个对象有个key值:"CountryName",对应的value:"Austria" 就是国家名称。这里的对象在数组中没有按字典排序,后续需要进行处理。

img

  1. 然后点开key值:"Participations",对应的value又是一个数组,数组里面每个对象都包含"Gender""PreferredLastName""PreferredFirstName"三个属性,正是我们需要的。在同一个国家中的每个运动员,已经按选手的名(Last Name)为次要关键字升序排序了,后续无需处理。

img

2.1.3 解题思路

  • 当DWASearch.java中的Main函数确认要执行players命令后,调用SearchHelper.java接口中的searchAthletes方法。使用Java中第三方库org.json解析Json文件。
  • 由于“==3. 顺序以国籍为首要关键字升序、选手的名(Last Name)为次要关键字升序排序(若爬取的数据已经排序完成则仅需要依次提取需要的信息,不必编写排序过程的代码)。==”,我使用TreeMap对整个国家的所有运动员为一个整体进行排序,key为"CountryName",value为某个国家运动员信息的对象。
  • 经过TreeMap排序后的键值对依次进行处理,对每个key为"CountryName"JSONObject,用getJSONArray方法解析Json中key为"Participations"的数组
  • 遍历由以上得到的数组,对数组中的每个对象,使用getInt方法或者getString方法得到对应的性别"Gender"、姓名"PreferredLastName""PreferredFirstName",按要求格式保存到BufferedWriter,最后输入到output文件中。

2.1.4 独到之处

使用TreeMap对输出内容进行排序,利用现有容器,代码简洁明了。

2.2 问题2

2.2.1 JSON资源获取

  1. 同2.1.1,在比赛结果中打开开发者工具。

  2. 同2.1.2、2.1.3,点击“RESULTS”中的某项目发起网络请求,找到我们需要的比赛结果的Json文件并保存下来。

img

  1. 同2.1.4、2.1.5保存Json文件后用VSCode打开文件进行格式化,然后进行下一步分析

2.2.2 分析JSON资源结构

  1. 分析比赛项目“Women 1m Springboard”的Json文件,发现我们需要的数据的key值为"Heats",其对应的value是一个数组,数组中的对象表示决赛、半决赛和预赛的成绩,由key为"Name"区分。

img

  1. 然后在决赛"Final"所在的对象中,查看key为"Results"对应的数组,数组中的每个对象就是根据排名"Rank"排序的比赛结果,总得分key值"TotalPoints"对应的value为"248.95",排名key值"Rank"对应的value为1。详细得分在key值"Dives"对应的数组中得到,数组的每个对象就是不同评委的打分,例如第一个评委打分key值"DivePoints"对应的value为"51.60",其余同理可以在组数的对象中得到。

img

2.2.3 解题思路

  • 由于共有10个比赛项目,每个项目都有可能作为result命令的对象,并且一个比赛项目对应一个Json文件,如果为每个比赛项目都专门写一个方法,而又每个比赛项目的结果Json文件结构一致,将会有大量重复的代码。所以需要用一个方法来处理不同的10个项目的文件。
  • 为了解决不同命令能打开不同的文件,我首先对Json文件重命名为项目名称,使得程序可以通过输入的命令打开对应的文件。这样,当命令中出现哪个项目,就通过文件名打开对应的项目Json文件。完成这一步,只需要实现动态文件名。只需要对命令进行处理,转化成文件名,方便程序找到文件并打开文件。
  • 由于本题已经按照排名"Rank"排序了,所以本题只需要按顺序输出就好,无需排序。本题的难点在于,每个评委的打分在Json中嵌套得比较深,所以只需要细心编写代码,正确解析ObjectArray,就没有什么大问题。
  • 最后同样按要求格式保存有关数据到BufferedWriter,最后输入到output文件中。

2.2.4 独到之处

实现动态文件名,根据输入指令,转化成对应将要访问的文件名。

3. 接口设计和实现过程

3.1 接口设计

根据需求,我们需要实现两个功能:打印全部运动员打印全部比赛结果。为此,我们考虑使用 ==Java 8== 引入的新特性,即在接口中使用静态方法。这样一来,继承者可以直接调用接口中的静态方法。

  • 我们首先需要实现一个名为 searchAthletes 的静态方法,内部使用 org.json 库解析 JSON 数据,并根据题目要求进行打印。
  • 接着,我们需要实现另一个名为 searchResult 的静态方法,根据传入的项目名称,打开对应的 JSON 文件,同样使用 org.json 库解析 JSON 数据,并按照题目要求进行打印。
  • 最后,为了处理错误的输入数据或者在其他地方需要直接向输出文件中写入内容,我们实现了名为 writeTxt 的静态方法。输出内容和指定输出文件都由方法参数传入。

3.2 实现过程

  • 根据上述分析,我们需要先导入第三方库文件 org.json 的 JAR 包。然后在接口代码中导入 org.json.JSONArrayorg.json.JSONObject。这样我们就可以调用 org.jsonJSONArrayJSONObject 类去解析相应的数据。
  • 接着,根据以上接口设计,我们编写了三个部分的代码。
  • 在主函数所在的主类 DWASearch 中,我们实现了 implements SearchHelper,这样我们就可以在任意地方通过调用接口 SearchHelper.searchAthletes(outputFile)SearchHelper.searchResult(event, outputFile)SearchHelper.writeTxt("someThing", outputFile) 得到相应的处理方法。

4. 关键代码展示

  1. 判断命令输入的正确性和处理

    while ((line = br.readLine()) != null) {
        command = Arrays.asList(line.split(" "));//把line的内容按单词放到commond中
        if (command.get(0).equals("players")) {
            SearchHelper.searchAthletes(outputFile);
        } else if (command.get(0).equals("result")) {
            if(command.size() < 4) {
                SearchHelper.writeTxt("N/A\n-----\n",outputFile);
                continue;
            }
            String event = command.get(1) + " " + command.get(2) + " " + command.get(3);
            if (events.contains(event) && command.size() == 4) {
                SearchHelper.searchResult(event,outputFile);
            } else if (events.contains(event) && command.get(4).equals("detail") && command.size() == 5) {
                SearchHelper.searchDetail(event,outputFile);
            } else {
                SearchHelper.writeTxt("N/A\n-----\n", outputFile);
            }
        } else {
            SearchHelper.writeTxt("Error\n-----\n", outputFile);
        }
    }
    
  2. org.json的使用

    while ((line = reader.readLine()) != null) {
        jsonContent.append(line);
    }
    JSONArray athletesArray = new JSONArray(jsonContent.toString());
    for (int i = 0; i < athletesArray.length(); i++) {
        JSONObject Country = athletesArray.getJSONObject(i);
        String countryName = Country.getString("CountryName");
        countryList.put(countryName, Country);
    }
    
  3. 打开动态文件名

    String event_ = event.replace(" ", "_");
    String inputFilePath = "222100129/datas/results/" + event_ + ".json";// 构建结果文件的路径
    try (BufferedReader reader = new BufferedReader(new FileReader(inputFilePath));
         BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, true));
    )
    { ... }
    

5. 性能改进

5.1 使用 HashMap 实现缓存机制

在处理输入指令时,可能会出现重复的情况。如果每次重复指令都执行相同的文件读写操作,将导致大量时间的浪费。考虑到计算机存储成本较低于时间成本,我们可以采用空间换时间的思路,即使用 HashMap 存储键值对作为缓存机制的实现体。

  • 在这个实现中,我们将指令作为键(key),相应的输出文件内容作为值(value)。这样,每次接收到指令时,首先检查 HashMap 中是否存在对应的指令。如果存在,则直接从 HashMap 中获取相应的内容并输出到输出文件中。如果没有找到,则执行正常的文件读写操作,并将结果保存到 HashMap 中。

  • 通过这种方式,从第二次执行相同指令开始,就可以减少代码运行所需的计算时间。由于指令数量是有限的,因此不会占用大量内存。

  • 这种优化思路充分利用了 HashMap 的快速查找特性,避免了重复读取相同指令时的不必要的文件读写操作,从而提高了程序的执行效率。

HashMap<String, String> cache = new HashMap<>();
...
if (cache.containsKey("players")) {
    writeTxt(cache.get("players"), outputFile);
    return;
}
...
writer.write(output.toString());
cache.put("players", output.toString());

5.1.1 改进效果

对searchAthletes方法执行两次,分别观察两次运行的耗时。
测试代码如下:

@Test
public void searchAthletes() {
    long startTime=System.currentTimeMillis(); //获取开始时间
    SearchHelper.searchAthletes("output.txt");
    long endTime1=System.currentTimeMillis();
    long time1 = endTime1-startTime;
    SearchHelper.searchAthletes("output.txt");
    long endTime2=System.currentTimeMillis();
    long time2 = endTime2-endTime1;
    System.out.println("time1: "+time1+"ms");
    System.out.println("time2: "+time2+"ms");
}

运行结果如下:

img

5.2 使用BufferedWriter提高效率

在处理输入流时,我们发现使用 FileWriter 进行写操作速度较慢。这可能是因为 FileWriter 每次写入数据时都会立即将数据刷新到磁盘,而这种操作对于大量数据的写入来说可能效率较低。

  • 为了提高写操作的效率,我们可以使用 BufferedWriter 类来包装 FileWriter,从而对输出流进行更加精细的控制。
  • BufferedWriter 会在内部维护一个缓冲区,在写入数据时先将数据存储在缓冲区中,当缓冲区满了或者手动调用 flush() 方法时,再将数据一次性刷新到磁盘上,这样可以减少频繁的磁盘IO操作,提高写操作的效率。
  • 通过使用 BufferedWriter,我们可以有效地减少磁盘IO次数,从而提高了写操作的速度和效率,尤其是在处理大量数据时表现更加突出。
try (BufferedReader reader = new BufferedReader(new FileReader("222100129/datas/athletes.json"));
     BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, true))){
    ...
}

以下是改进前的代码:

StringBuilder jsonContent = new StringBuilder();
int character;
while ((character = reader.read()) != -1) {
    jsonContent.append((char) character);
}
JSONArray athletesArray = new JSONArray(jsonContent.toString());

以下是改进前的执行时间:

img

以下是改进后的执行时间:

img

6. 单元测试

在Java开发中,为了确保代码的质量和可靠性,我们经常需要进行单元测试。JUnit是Java领域最流行的单元测试框架之一,它提供了一种简单而强大的方式来编写和运行测试用例。

通过JUnit,我们可以轻松地对代码的各个部分进行测试,包括函数、方法、类等。这样一来,我们可以在开发过程中不断验证代码的正确性,减少BUG的产生,并提高代码的可维护性和可读性。

6.1 DWASearch类测试

测试代码:

public class DWASearchTest {

    @Test
    public void test1(){
        //错误的参数输入数量
        DWASearch.main(new String[]{"input.txt"});
    }
    @Test
    public void test1_1(){
        //错误的参数输入数量和文件后缀
        DWASearch.main(new String[]{"input"});
    }
    @Test
    public void test2(){
        //正确的参数个数输入
        DWASearch.main(new String[]{"input.txt","output.txt"});
    }
    @Test
    public void test2_1(){
        //正确的参数个数输入
        DWASearch.main(new String[]{"input.txt","putout.txt"});
    }
    @Test
    public void test2_2(){
        //错误的参数输入文件后缀
        DWASearch.main(new String[]{"input.txt","putout"});
    }
    @Test
    public void test2_3(){
        //错误的参数输入文件后缀
        DWASearch.main(new String[]{"input.txt","putout.p"});
    }
    @Test
    public void test3(){
        //不正确的参数输入数量
        DWASearch.main(new String[]{"input.txt","output.txt","redundant.txt"});
    }
    @Test
    public void test3_1(){
        //不正确的参数输入数量和文件后缀
        DWASearch.main(new String[]{"input.txt","output","redundant.txt"});
    }
}
  • test1测试结果:

img

结果分析:因为DWASearch类中没有嵌套类,所以类的覆盖率100%;且DWASearch类中只有一个主函数Main,所以方法覆盖率也是100%;但因为参数个数错误,抛出异常,提前终止程序,所以代码行的覆盖率为23%。

  • test1_1测试结果:

img

  • test2测试结果:

img

  • test2_1测试结果:

img

  • test2_2测试结果:

img

  • test2_3测试结果:

img

  • test3测试结果:

img

  • test3_1测试结果:

img

以上所有结果符合预期

6.2 input.txt指令内容测试

测试代码:

@Test
public void searchAthletesTest(){
    //不正确的参数输入数量和文件后缀
    DWASearch.main(new String[]{"input.txt","output.txt"});
}

input.txt内容:

img

output.txt内容:

img

所有错误指令能够正确识别,符合预期

6.3 提升覆盖率

测试代码:

@Test
public void testCoverage(){
    //正确的参数个数输入
    DWASearch.main(new String[]{"input.txt","output.txt"});
    DWASearch.main(new String[]{"input.txt"});
    DWASearch.main(new String[]{"nonexistence.txt","output.txt"});
}

input.txt内容:

img

测试覆盖率结果:

img

类和方法的覆盖率都达到100%了,只有代码行的覆盖率达不到100%。返回项目源代码页面,查看代码行数标号旁边的颜色,如果是绿色,表示刚才测试执行了这行代码。我们找红色的代码行:

img

上图说明我们在searchAthletes方法中没有执行第二次players指令。现在我把players放到input.txt文件末尾,让程序执行第二次players指令。如下图所示input.txt内容:

img

再次执行测试代码,运行结果如下:

img

由上图可知,代码行覆盖率由96%提升到97%,并且行数标号旁边的颜色变成绿色:

img

其余未执行的代码行均为打开output.txt文件写数据,如果不发生磁盘已满、文件被其他程序锁定等情况,是无法执行catch代码块中的代码:

img

6.4 自动单元测试

  1. 安装JUnit插件

img

  1. 选择要测试的类,鼠标右键,选择“Go To”,选择“Test”

img

  1. 选择“Create New Test...”

img

  1. 根据需要挑选Member

img

7. 异常处理

7.1 指令错误

同4.1 判断命令输入的正确性和处理

7.2 IO异常

try (BufferedReader br = new BufferedReader(new FileReader(args[0]))) {
    ...
} catch (IOException e) {
    e.printStackTrace();
}

8. 心得体会

首先,我深刻体会到了使用不熟悉的爬虫、Git、GitCode、Maven等相关技术来完成项目的挑战。虽然我之前有一些接触,但对这些技能的实际运用了解不够深入,因此在项目中感到了“书到用时方恨少”的情况。

其次,我对软件工程的流程有了更深刻的理解。从需求分析、数据查找、项目设计、代码编写到项目测试,我亲身经历了整个流程。特别是项目测试阶段,我开拓了新的视野。我以前以为测试只是简单地运行代码并输入各种数据,但现在我学会了使用Junit进行测试,提高了测试的效率,还能通过覆盖率来完善测试。

最后,我深刻认识到单纯编写代码并不困难,真正的挑战在于提高程序的性能和容错性。这需要不断学习他人优秀的代码,丰富自己的经验。另外,我发现环境配置对项目时间的影响很大。我经常需要在网络上查找文章,甚至向同学请教,才能解决编译器的某些设置问题。但我相信practice makes perfect,发现自己的不足并不断完善学习,才能在下次遇到类似问题时更快地解决。

...全文
78 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧