一、前言
在第一次题集的基础上,这三次的大作业都考察了继承与多态。通过继承,一个子类可以获得父类的属性和方法,并且可以扩展或修改这些属性和方法。多态是指相同的操作作用于不同的对象时,可以产生不同的结果。它使得同一个方法调用可以根据不同的对象产生不同的行为。这提高了代码的可维护性、增强了系统的扩展性。
答题判断程序-4,这是答题判断程序的第三次迭代,相较于前三次的题目,新增了各种题型及其不同种类的答案,并且出现多选题、填空题,输出方面的要求也更高了。具有很大的挑战性。
家居强电电路模拟程序-1,这是一次全新的题目,需要我重新构思设计,在编写代码之前就考虑好类与类之间的关系。这道题难点在于电器之间的连接,电路是否为通路,电器状态的改变,电器对电压的改变,电压的传导和电器功能的展示。在输出时也必须按照要求顺序输出。
家居强电电路模拟程序-2,这道题是在家居强电电路模拟程序-1的基础上进行迭代,虽然仅仅只增加了新的用电器,并联电路和电阻值这三个操作,但带来的改变同上一次十分巨大,通路的判断不再只是判断是否开关全部闭合,而是要判断多个控制设备的开闭及其位置,并联电路和电阻值的加入使得电压的判断更加困难,但我考虑引入了电流,通过电流来计算各个设备的分压,从而解决了设备压差驱动工作的问题。
尽管这几次的作业具有极大的挑战性,但通过不断地思考、实践和反思,使我们能够逐渐掌握家居强电电路的模拟技能,为未来的学习和职业发展打下坚实的基础。
二、设计与分析
1.答题判题程序-4
- 在前几次迭代的基础上,我添加了选择题类继承自问题类。
选择题的难点在于多选题,判断结果除了true、false,增加一项”partially correct”表示部分正确。于是我考虑首先用一个Set集合人来保存标准答案的所有关键字,再将用户输入的答案按空格拆分为列表。然后根据匹配情况返回不同的字符串。如果用户提交的答案与标准答案完全一致(正确答案的个数和提交的答案个数都相等,并且都是正确答案),返回 true
,表示完全正确。如果用户部分正确(正确答案的数量大于0并且小于正确答案个数,且没有错误答案),返回partially correct
。否则返回false
。
总结来说,这个类交的答案与标准答案的对比,判断是否完全正确、部分正确或不正确,并返回相应的结果。
if (submittedAnswers.isEmpty()) {
return "false"; // 没有答案
} else if (correctCount == correctAnswers.size() && correctCount == submittedAnswers.size()) {
return "true"; // 完全正确
} else if (correctCount > 0 && correctCount < correctAnswers.size() && submittedAnswers.size() == correctCount) {
return "partially correct"; // 部分正确
} else {
return "false"; // 包含错误答案或其他情况
}
- 新增填空题类继承自问题类
与选择题类类似,但用户提交的答案不再以空格分割,如果用户的答案与标准答案完全一致,返回 true
,表示答案完全正确。如果用户的答案是标准答案的一部分,返回 partially correct
,表示部分正确。
if (answer == null || answer.trim().isEmpty()) {
return "false"; // 没有答案
}
// 判断用户提交的答案与标准答案的关系
if (answer.equals(standardAnswer)) {
return "true"; // 完全正确
} else if (standardAnswer.contains(answer)) {
return "partially correct"; // 部分正确
} else {
return "false"; // 错误答案
}
- 判分
如果在识别到是半对的时候,分数为原来的一半。
- 新增解析输入类
在之前的三次设计中,我在主函数中放入大量输入的字符串的解析方法,因为主函数的责任并不仅限于控制程序的执行流程,它承担了过多的工作———不仅仅是调用其他模块,还包括了输入字符串的解析过程。这使得主函数变得复杂且不易维护。这位违背了面向对象程序设计的单一职责原则。第四次迭代我则改正了这个问题,将所有解析输入字符串的代码封装到一个类中,避免主函数中代码过于复杂。如果未来需要修改输入解析的逻辑或更换解析方式,只需要修改这个专门的类,而不必影响主函数的代码结构。这种解耦的设计提高了程序的可扩展性与灵活性,减少了修改时带来的潜在问题,提高了代码的可读性和可重用性。
- 输出顺序优先级为学号、试卷号,按从小到大的顺序先按学号排序,再按试卷号。
首先,比较两个对象中 studentId
字段的大小(通过 student.getStudentId()
获取学生 ID)。如果两个学生的 ID 相同,则继续比较两个对象的isTestExists
字段,判断测试是否存在:
如果两个对象都没有测试(isTestExists 为 false
),则它们视为相等,返回 0。
如果当前对象的测试不存在而另一个对象的测试存在,则当前对象排在后面,返回 1。
如果另一个对象的测试不存在而当前对象的测试存在,则当前对象排在前面,返回 -1。
如果两个对象的测试都存在,则比较它们的测试编号(test.getTestNum()
),返回测试编号的比较结果。
public int compareTo(AnswerSheet other) {
int studentComparison = this.student.getStudentId().compareTo(other.student.getStudentId());
if (studentComparison != 0) {
return studentComparison;
} else {
// 检查两个 AnswerSheet 的测试是否存在
if (!this.isTestExists && !other.isTestExists) {
return 0;
} else if (!this.isTestExists) {
return 1;
} else if (!other.isTestExists) {
return -1;
} else {
return Integer.compare(this.test.getTestNum(), other.test.getTestNum());
}
}
}
- 类图设计
类图并不是一次性设计完成的,它应该是一个迭代的过程。在开发过程中,随着需求的变化和系统复杂度的增加,类图可能会发生变化。因此,设计类图时要具备灵活性,并在开发过程中不断优化。与前三次迭代不同的是,多了两个选择题类与填空题类均继承自问题类,并且新增了输入解析类和学生类。符合单一职责原则。
2.家居强电电路模拟程序-1
- 设备类
首先我设计了一个抽象类设备类,用于存储所有设备共性,如设备编号、设备名称、引脚电压,由父类设置功能性方法入口,由继承子类重写方法并具体实现,各子类设置独属于自己的参数,如转速、亮度、档位等等,并根据各自功能实现不同重写父类方法。其中要注意父类的属性设置为protected
,使用protected
可以使得父类的属性在其子类中可以直接访问和修改。虽然private
可以确保数据封装性和类内部的安全性,但它不能允许子类访问这些属性。而protected
则允许继承了父类的子类直接访问这些属性,从而能够方便地在子类中重用和操作父类的属性。
- 控制设备类和受控设备类
接着我设计了控制设备类和受控设备类都都是抽象类,继承设备类,并为不同类型的控制设备(如开关、灯和风扇)提供了一个统一的接口。每个设备类都通过updateStatus()
方法根据输入电压来调整设备的状态,并通过 display()
方法显示设备状态。其中每个具体的设备都有自己的实现这体现了面向对象程序的单一职责原则。
控制设备中包括开关、分档调速器、连续调速器。子类通过多态实现了不同类型的控制设备的行为(如开关切换、速度级别调节)。这增加了代码的可维护性和可扩展性,允许开发者在不修改现有代码的基础上添加新设备类型。
受控设备包括灯、风扇。两种设备都有两根引脚,通过两根引脚电压的电压差驱动设备工作。使用继承和多态来处理不同类型设备的相似操作,提高了代码的复用性和可扩展性。如果需要新增设备类型,只需要继承受控设备类并实现相应的方法,避免了大量的重复代码。
- 电路类
最后我设计了电路类,使用HashMap来存储设备实例,其中键值是设备id,值是设备对象。而连接信息使用一个存储 String[] 数组的列表,来保存连接在一起的两个设备。
private final Map<String, Device> devices = new HashMap<>();
private final List<String[]> connections = new ArrayList<>();
然后实现添加设备方法、添加连接信息方法、传输电压方法、更新设备状态和输出设备等方法。其中难点在于设备电压的传输,核心思想为:在电路中开关全闭合的情况下,如果连接的源是 "VCC",则给目标设备提供输入电压 220V。如果连接的目标是 "GND",则设置源设备的输出电压为 0。否则,通过设备之间的电压传输,源设备的输出电压作为目标设备的输入电压。
其次在输出顺序上我也遇到了问题,通过上网查找,我学会了一种方法。这里创建了一个 Map<Character, Integer>
类型的 deviceOrder 字典,用于定义设备类型(假设通过设备的 ID 字符串的第一个字符来标识)与优先级的映射关系。例如,'K' 类型的设备优先级为 1,'F' 类型的设备优先级为 2,依此类推。在排序时,设备会按照以下规则排序:
设备类型:K -> F -> L -> B -> R -> D
如果类型相同,按 ID 后面的数字进行排序。
public void displayAllDevices() {
List<Device> deviceList = new ArrayList<>(devices.values());
// 设备类型顺序定制
Map<Character, Integer> deviceOrder = new HashMap<>();
deviceOrder.put('K', 1);
deviceOrder.put('F', 2);
deviceOrder.put('L', 3);
deviceOrder.put('B', 4);
deviceOrder.put('R', 5);
deviceOrder.put('D', 6);
// 按类型优先级和ID编号排序
deviceList.sort((d1, d2) -> {
int typeCompare = Integer.compare(deviceOrder.get(d1.getId().charAt(0)), deviceOrder.get(d2.getId().charAt(0)));
if (typeCompare != 0) {
return typeCompare;
}
// ID编号比较
return Integer.compare(Integer.parseInt(d1.getId().substring(1)), Integer.parseInt(d2.getId().substring(1)));
});
// 输出设备信息
for (Device device : deviceList) {
device.display();
}
}
- 解析器类
吸取了上一次的教训之后,在本次设计中我也添加了一个解析输入信息的类,避免主函数过于复杂。我会根据输入信息前的符号不同进行分割,如 [ 与 # 进行不同的分割。如果行以 [ 开头并以 ] 结尾,表示这是一个设备连接的描述,使用 split(" ")
将连接字符串拆分为两个部分,将这些连接信息传递给电路,表示这两个设备在电路中相邻,并添加设备。对于以 # 开头的行,会解析设备状态更新,例如开关是否闭合,调速器的挡位等。
- 主函数
主函数读取多行用户输入,直到输入 "end",然后将这些输入传递给解析器类进行解析,最后输出解析结果。
以上就是我全部类的设计。
- 类图设计
开关、调速器继承控制设备类,灯和风扇继承受控设备类,而这两个类又继承设备类。电路类与设备类的关系是聚合关系。
2.家居强电电路模拟程序-2
第二次迭代比第一次要考虑的东西就多多了,在第一次迭代的基础上,我添加或修改以下类。
- 电路类
将电路类抽象并继承设备类,这个好处就是,可以将整个并联电路看作一个设备,给它进行电压传输等操作。
- 串联电路类
新增串联电路类继承电路类,添加方法计算串联电路总电阻,通过总电阻可以计算出电路中的电流,利用电流计算出每个设备得到的分压,这样大大方便了传输电压这一个操作。这其中有一个巧思就是,只有当检测到这条电路中开关闭合了后才将相应电路上的设备阻值加上去,否则阻值就为0。
public void get_allResistance(){
if(this.judge_closed()){
for (Device device:devices.values()) {
resistance += device.getResistance();
}
}
}
- 并联电路类
新增并联电路类继承电路类,表示一个包含多个串联电路的电路系统。并通过HashMap来添加串联电路并管理串联电路,每个串联电路通过其 id(作为键)与其对应的 Series_Circuit 实例关联。与串联电路相同,需要计算整个并联电路的阻值来计算整个并联电路在整个电路中的压差,并将这个压差传入各条串联电路中。
public void get_allResistance(){
for (Series_Circuit series:seriesMap.values()) {
if(series.judge_closed()){
series.get_allResistance();
resistance += 1.0/series.getResistance();
}
}
resistance = (resistance == 0) ? 0.0 : 100000/(int)(resistance*100000);
}
- 复合电路类
最后新增复合电路类,及既有并联电路又有其他单个设备的电路,由于可以将整个并联电路看作一个设备,与串联电路类似,所以我将复合电路类继承串联电路类。创建HashMap来储存将并联电路看作一个设备后的所有设备。重写传输电压方法,同样利用压差来更新设备状态。将设备更新和设备输出也放入本类中。
- 解析器类
与第一次设计不同,我使用正则表达式将每一行输入与不同类型的电路进行匹配,并根据匹配结果构建电路实例,建立它们之间的关系(如将多个串联电路添加到一个 Multiple_Circuit 系统中)。
private final Pattern pattern1 = Pattern.compile("#(T\\d+):\\[IN.*");
private final Pattern pattern2 = Pattern.compile("#(M\\d+):.*");
private final Pattern pattern3 = Pattern.compile("#(T\\d+):\\[VCC.*");
private final Pattern pattern4 = Pattern.compile("#[KFL]\\d+.*");
- 类图设计
与上一次不同的是,这次迭代我将将各类分的更加仔细,单一职责原则更体现出来。
三、踩坑心得
1.在答题判断程序-4中,由于一开始并没有将学生单独封装成一个类,而是将其作为答卷类的一个属性,导致程序无法对多试卷多学生的情况进行解析,造成了非零返回的情况。
改正:在明白这个问题之后,我立马将创建了一个学生类,将所有学生信息单独封装在这个学生类中,创建一个HashMap
来存放每个学生的信息,解决了多学生多试卷的问题。
但是最终最后两个测试点还是过,在测试了各种各样的样例之后还是不知道测试点在哪。
2.空指针异常:Java中最常见的错误之一。它发生在尝试访问空对象的方法或属性时。要避免这种情况,应该使用null检查或者合适的异常处理机制。在许多地方要考虑是否有可能为空值。
3.答题判断程序-4中,在获取填空题信息时不能随便去掉空格,这会影响最终的判题,我尝试用或去分割,但是好像也不太行,最终我选择使用contains(answer)
包含相关字符来判断是否部分正确。
4.在家居强电电路模拟程序-1中,一开始我发现由于受控设备是根据压差来工作的,如果在这个受控设备后面有一个断开的开关,而此时受控设备的输出电压等于断开的开关此时的输入电压,由于开关时断开的导致受控设备还是具有压差也能正常工作,这是不符合常理的,所以我在进行电压传输这一步操作的时候,首先判断整个电路是否有开关打开,由于只考虑串联电路,所以只要电路中有一个开关没有闭合,就不进行传输电压的操作,即所有设备电压均为初始值0。
public boolean judge_closed(){
List<Switch> list = new ArrayList<>();
boolean flag = true;
for (Device device : this.devices.values()) {
if (device.getId().startsWith("K")) {
list.add((Switch) device);
}
}
for (Switch Sw : list) {
if (Sw.getStatus().equals("turned on")) {
flag = false;
break;
}
}
return flag;
}
5.在家居强电电路模拟程序-2中,我遇到了与前一次类似的问题,这次不能仅仅判断是否电路中的所有设备是否均关闭了,因为在并联电路中,如果有一条电路中的开关断开了,而另一条电路的开关是闭合的,也可以形成通路,所以我编写了一个用于判断是否并联电路中的所有串联电路的开关都打开的函数,如果不是都打开的情况下便可以传输电压,形成一条通路。
public boolean judge_opened(){
boolean flag = true;
for (Series_Circuit series:this.seriesMap.values()) {
if(series.judge_closed())
flag = false;
}
return flag;
}
所以在复合电路中我要同时判断不包括并联电路开关在内的所有开关是否处于闭合状态和并联电路中的每条串联电路开关是否不处于都打开的情况下,而并联电路中的每条串联电路也要分别讨论开关是否完全闭合,只有满足了这些条件,才进行电压传输的工作,否则没有任何意义。
6.在家居强电电路模拟程序-2中,还要考虑短路的情况,在我的设计中,如果并联电路中存在短路的情况,那么相当于并联了一条导线,由于导线电阻为0,在计算并联电阻时resistance += 1.0/series.getResistance();
就等于无穷大,在最后计算总阻值为无穷大的倒数即为0,总阻值为0就分不到电压,等同于短路。
但是最后我有两个测试点就是过不去,实在找不出哪里有问题了。
询问了同学居然发现没通过的测试点一样,在他的帮助下,发现是计算时的精度问题,在计算并联电路总阻值时,resistance = (resistance == 0) ? 0.0 : 100000/(int)(resistance*100000);
分子分母同乘100000,居然就通过测试点了,神奇!
四、改进建议
1.圈复杂度高:代码中存在较多的条件分支和循环,程序的逻辑更复杂。较高的圈复杂度增加了出错的可能性,也会使得单元测试和后期维护变得更加困难。圈复杂度是评估Java代码复杂性的重要指标之一,帮助开发人员评估代码的可维护性和测试难度。理解和管理圈复杂度有助于提高代码质量,减少错误,降低维护成本。在编码过程中,合理设计条件分支和循环,避免过于复杂的嵌套,能有效地保持圈复杂度在一个合理的范围内。可以通过重构复杂方法或提取公共逻辑到单独的方法或类中来降低复杂度;
2.重复代码:代码的重复率较高,通常可以在项目中减少重复代码。使用设计模式、重构和封装重复逻辑可以减少代码的冗余,提高代码的质量和可维护性。
3.注释率低:有些文件的注释率非常低,注释对于代码的可读性和可维护性至关重要。
五、总结
这三次作业都体现了类设计的重要性,一个合理的类设计可以使我们在编写程序时更加流畅,错误更少。
在这三次作业中我也体会到了熟练运用继承与多态的好处,继承允许子类从父类中继承已有的属性和方法,避免了重复编写相同的代码。这样可以提高代码的可维护性和可扩展性,减少了开发时间和工作量。通过继承,可以建立起类之间的层次结构,形成更加有组织和易于理解的代码结构。父类定义了通用的属性和方法,而子类可以在此基础上进行扩展和特化。继承与多态性密切相关。多态性指的是基于基类的引用,可以调用子类的方法。这样可以实现接口的统一性,并且能够根据实际情况动态选择具体的实现。当需要对现有的类进行修改或扩展时,通过继承可以方便地添加新的功能或修改现有的功能,而无需修改父类的代码。这符合开放封闭原则,即对扩展开放,对修改封闭。使用继承可以帮助开发者更好地组织和管理代码。通过将共享的属性和方法放在父类中,可以使代码更具有结构性和可读性,便于团队协作和代码维护。
总之,继承提供了一种有效的代码复用机制,并且能够建立起类之间的层次结构,使代码更具有灵活性和可扩展性。通过合理应用继承,可以提高代码的质量和效率,同时减少错误和冗余的代码量。
这些基础知识的运用不仅提高了我的编程技能,也加深了我对面向对象编程思想的理解。我逐渐认识到,在编写代码时,类之间的设计是至关重要的。良好的设计能够使代码更加模块化、易于维护,并且更加符合面向对象编程的原则。因此,我不断思考并尝试优化类之间的设计,努力让我的代码更加面向对象,更加高效和可复用。这些经历不仅锻炼了我的编程能力,也培养了我解决问题的思维方式和团队协作能力。我相信,在未来的学习中,这些经验和技能将为我带来更大的帮助和成就。