第二次Java大作业总结
Java大作业总结
一.前言
这三次大作业,第四次是连接着前三次的,由于还有填空题题和多选题的出现,判分的逻辑要改变,同时还增加了正则表达式判断格式,输出的逻辑也要改变,这次代基本上是对上次代码的大改,只有输入的逻辑和之前两次差不多,其他逻辑都改变了。第五次大作业是新的开始,比较简单,我也没有用到电路这个类和引脚这个类,但这导致第五次代码沿用第四次的根本写不出来,于是又重写了一遍加了串联电路类和并联电路类。这三次大作业都用到了继承。同时也都用到了集合和集合的遍历。还有类里面重写toSting
方法。对于集合的遍历的方法,很多都是Java特有的,在C语言未曾见过的语法。
第四次大作业
1.设计与分析
这次的代码的输入逻辑我沿用了上次大作业的逻辑,同时也增加了分割多选题和填空题的代码逻辑。把单选题,多选题,填空题抽象出来,变成了类,这三个类分别是oneChoice
multipleChoice
fillQuestions
,同时它们都继承至Question
这个类,这个类又有id
content
和Answer
这三个成员变量。同时还有类QuestionMap
,这个类中的成员变量private Map<Integer, Question> questionIDAndQuestion = new LinkedHashMap<>();
存储所有题目的信息。
- 在
scanningPaper
这个类的职责是存储输入的信息。先读取一行的信息判断输入信息是否是end,如果不是就用StringBuffer
存储同时还在每一行的末尾存储换行符\n
。 - 接着便是
Split
这个类,它获取到刚刚存储到的信息,按开头字母的进行题目的划分,创建相应单选题,多选题和填空题的对象,接着存储到questionIDAndQuestion
里面。在划分时,还有进行题目格式的判断,于是我定义了一个checkFormat
类,来检查题目格式是否正确,若不正确,提示信息,同时跳过这个题目的存储。在#T的分割中,我一边存储信息,也一边算总分是否达到了100分,若没有达到也直接提示对应信息。还有学生类中,定义了一个Map的键值对来存储学生的学号和姓名。也有删除题目类,我用Set集合存储删除题目的题号。至此输入信息,分割和存储题目信息等工作都完成。检查格式的正则表达式如下:
String check = "#N:\\d+(\\s+#Q:.+?\\s+#A:.+?)";
String check = "#Z:\\d+\\s+#Q:.+\\s+#A:.+";
String check = "#K:\\d+\\s+#Q:[^#]+\\s+#A:[^#]+";
String check = "#T:\\d+(\\s+\\d+-\\d+)+";
String check = "#X:\\d+\\s+\\w+(-\\d+\\s+\\w+)*";
String check = "#S:\\d+\\s+\\d+(\\s+#A:\\d+-.+)+";
-
接着便是评分类
rate
,这里用到了很多Java特有的集合的遍历和Lambda表达式,比如split.getAnswerPaper().AnswerSheets.forEach((paperId, studentMap) -> studentMap.forEach((studentId, Answers) -> studentAnswers.computeIfAbsent(studentId, k -> new HashMap<>()).put(paperId, Answers)));
这里就是先遍历
AnswerPaper
中的AnswerSheets
集合,同时这个集合中又存储了studentAnswers
这个集合,于是有对学生的答案信息进行遍历,从这个集合中获取学生的学号,同时从AnswerSheets
集合获取学生输入的答案,接着用Map<String, Map<Integer, List<Answer>>> studentAnswers = new HashMap<>();
存储起来,学号最为键,对应学号的学生的答案存在List
集合里面作为值。因为最后输出答案要按学号顺序输出,于是又对这个Map进行按学号排序,最终遍历整个排序好了的集合,通过学号获取到试卷的Id,然后通过试卷的Id获取题目信息,遍历题目信息,对答案为空,被删除的题目和题目本就不存在这三种情况进行过滤,输出对应的信息。若是没有这三种情况,就开始评分.- 单选图评分最简单,
return question.getAnswer().equals(givenAnswer.trim());
直接比较学生给的答案和标准答案是否符合,返回值为布尔类型。 - 在多选题中,先把存储标准答案的集合和学生答案的集合转为Set集合,接着用containsAll先比较是否包含全部内容,再比较Set集合大小,若是标准答案的Set集合全部包含了学生答案的Set集合,且2个Set大小一样,那么说明是全对的。若是标准答案的Set集合全部包含了学生答案的Set集合,但标准答案的Set集合大小大于学生答案的Set集合,那说明是少选。若是若是标准答案的Set集合没有全部包含了学生答案的Set集合,那就是错选了
if (studentAnswerSet.containsAll(standardAnswerSet) && standardAnswerSet.size() == studentAnswerSet.size()) { return true; } if (studentAnswerSet.containsAll(standardAnswerSet) && standardAnswerSet.size() < studentAnswerSet.size()) { return "partially correct"; } return false; }
- 在填空题中,获取到标准答案的字符串,再与学生的答案比较,若是equal,那就是全对。如果不是equal而是contain那就是半对,2中情况都不是那就是错填。
- 单选图评分最简单,
判完分最后就是输出了。
类图
顺序图
2.踩坑心得
-
刚开始由于错误使用equal方法,导致要是半对也会判成错误。
-
遍历题目时,删除的题目可能还会遍历,导致根据题号获取题目时,题目对象为null报错。
3.改进建议
-
当前代码在很多地方直接使用了
System.out.println
来输出错误信息,如遇到无效问题或答案为空的情况。建议使用异常处理机制来替代,这样可以更好地控制程序流程,并且可以在更高层次捕获并处理这些异常。 -
checkCorrect
方法在oneChoice
,multipleChoice
, 和fillQuestions
类中实现了相似的功能,但各自独立实现。可以通过在基类Question
中定义一个默认的检查方法,并允许子类重写它来提高代码复用性。 -
在
ratingPaper
方法中,有多个循环嵌套,特别是当数据量较大时可能会导致性能瓶颈。考虑使用更高效的数据结构或算法来减少不必要的遍历次数,例如使用哈希表来快速查找答案。 -
rate
类中的ratingPaper
方法实现了评分逻辑,但该方法过于庞大,包含了太多的功能。可以考虑将其拆分为几个小的方法,每个方法负责评分过程中的一个具体步骤,比如获取学生答案、验证答案正确性等。这样做可以使代码更加模块化,易于维护和测试。 -
遵循Java命名约定,变量名应采用小驼峰式命名法(camelCase),如
AnswerContent
应改为answerContent
。 -
类名应该清晰地表达其用途,如
DeletionQuestion
可能更合适的名字是DeletedQuestionsManager
。
第五次大作业
1.设计与分析
看了题目知道这是第一次迭代,比较简单。我也按照题目信息,划分了device
这个类和它的子类controlDeceive
还有controlledDeceive
,其中controlDeceive
又有子类开关k,分档调速器f,还有连续调速器l。controlledDeceive
又有子类白炽灯b,日光灯r,和吊扇d。
-
device
类中inputVoltage,outputVoltage,默认都是220V,还有一个String类型的deceiveId。 -
在开关k类里面还有一个独属于它的属性link,默认为false,当检查到输入了#K时,就会调用k类的toggle方法,把link置为相反,同时输入输出电压都变成0V,表示断路。
-
在分档调速器f类中,也有它的私有属性gears,默认为0,通过这个参数,可以修改分档调速器输出的电压。当检查输入了#F+或#F-时就会调用addGears或sybGears方法,实现分档,最后调用这个类里面的SetOutputVoltage方法,就可以实现分档调速。
-
在连续调档器l,同样也有proportion表示挡位,也是调用SetOutputVoltage方法设置输出电压,输出的电压直接通过getInputVoltage获取输入电压在乘上挡位。还要注意的是,最后输出我是放在这个类中,重写
toSting
方法,我用了DecimalFormat
类实现输出只保留2为小数。 -
在柏白炽灯d类中,有表示亮度的属性lightIntensity,当输入的电压大于0时,通过公式
lightIntensity = (int) ((getOutputVoltage() - 10) * 5 / 7 + 50)
就可以计算出亮度。 -
在日光灯r类中,同样定义了Brightness,这里计算比较简单,亮度只有0和180。
-
在吊扇d类中,定义了speed,根据电压调节speed,在电压80~150时,根据公式
speed = (int) (4 * getOutputVoltage() - 240);
算出此时的电压,大于150V时直接设置speed为360,小于80V时speed设置为0。 -
此外还定义了一个
home
类。来操控以上这些类,在输入时,也是和上一次大作业一样,定义一个StringBuilder
类型变量,如果输入了end,就跳出循环,否则用该变量拼接该行后再拼接换行符\n
,再逐行判断如果这一行字符串开头是#,就进入getControlCommand方法,获取命令,否则就进入getDevice方法获取设备,我定义了一个Map存储这些设备private static Map<String, device> indexAndDeceive = new LinkedHashMap<>();
先根据开头首字母判断是哪一种设备,然后再构造对应设备的实例,分割字符串,获取设备Id,因为同一个设备Id会出现2次,所以如果indexAndDeceive里面没有值为这个Id,就存储进去,否则不存储。然后在getControlCommand方法中,判断这一行是包含K,F,L其中的哪一个字母,然后跳转到对应操作。
-
最后是输出,输出的顺序要按开关、分档调速器、连续调速器、白炽灯、日光灯、吊扇的顺序依次输出,所以先定义一个字符串数组,代表顺序,先遍历整个数组的内容,再在循环中遍历设备的Map,获取设备的Id,如果设备的Id的首字母是这个顺序就输出,否则就跳过
String[] order = {"K", "F", "L", "B", "R", "D"}; for (String type : order) { for (Map.Entry<String, device> entry : indexAndDeceive.entrySet()) { String deviceId = entry.getKey(); device device = entry.getValue(); if (deviceId.startsWith(type)) { System.out.println(device); } } }
类图
顺序图
2.踩坑心得
- 开始存储Id值时,只是把K,L,F...这些当成Id,但是后面发现应该把K1这种当成Id存储。
- 开始默认电压为220V,但是后面经过分档调速器和连续调速器时,电压会变小,但是我又没用到引脚,如果后面有多个电器,就会出现前面和后面的电器联系不上的情况,导致电压也不知道是多少了。
3.改进建议
-
类名应该使用驼峰式命名法(Camel Case),例如
ControlledDevice
而不是controlledDeceive
。 -
变量名和方法名也应该遵循驼峰式命名法,如
deviceID
和setDeviceID
。 -
避免使用容易引起误解的名称,如
deceiveId
,建议改为deviceId
。 -
抽象类:可以考虑将
device
类定义为抽象类,并在其中定义一些通用的方法,如turnOn()
和turnOff()
。 -
接口:可以定义一些接口来描述不同的行为,如
Switchable
、Dimmable
等,这样可以使代码更加模块化和易于扩展。 -
设备管理:
home
类中的设备管理可以使用更高效的数据结构,如HashMap
或TreeMap
,以便更快地查找和操作设备。 -
输入解析:输入解析部分可以使用正则表达式来简化和优化,避免手动分割字符串。
-
状态管理:可以使用枚举类型来管理设备的状态,如
ON
、OFF
等,这会使代码更加清晰。 -
电压设置:在
f
和l
类中,电压设置的逻辑可以提取到一个单独的方法中,以减少重复代码。 -
输入验证:在解析输入时,应添加更多的异常处理,确保输入格式正确,防止程序因错误输入而崩溃。
-
边界条件:在设置电压和速度等参数时,应检查边界条件,确保不会出现非法值。
第六次大作业
1.设计与分析
由于上次大作业设计的不好,这次大作业重写了一边类和逻辑,现在我定义了一个电路设备类CircuitDevice
用来被继承,这个类属性有id,resistance,voltage,outputVoltage,inputVoltage。和上次一样,定义了受控设备类ControlledDevice
和控制设备类ControlDevice
继承它,这两个类有被其他类继承。在开关类的构造方法中,直接报开关电压置为-1,代表开路。若开关闭合,则设置电阻为0Ω。其他控制设备类也是和上次一样,只是电阻要设置为0Ω。其他的受控设备也是和上次一样,多的一步就是要在构造方法里面设置电阻。同时我还定义了一个电源类和一个地类它们都继承电路设备类。特别的是,根据老师上课讲的思路,我把并联电路也继承了电路设备类,当成一个设备,这样好算电阻和电压。
-
在输入时,判断开头,如果是“#T”就调用parseSeriesCircuit方法,如果是“#M”就调用parseParallelCircuit方法,否则调用parseOperation方法。同时输入解析类中有3个成员变量。
private Map<String, CircuitDevice> devices; // 所有设备的映射 private List<SeriesCircuit> seriesCircuits; // 串联电路列表 private List<ParallelCircuit> parallelCircuits; // 并联电路列表
-
在parseSeriesCircuit方法中,先过滤IN,OUT,VCC,GND这些字符串,然后判断是哪一个设备,然后构造该设备的实例,添加到devices这个Map集合,同时这个方法创建了一个SeriesCircuit实例,这个类里面有
private List<CircuitDevice> devices; // 串联电路中的设备列表
,会把设备的实例也添加到这个List里面,之后把SeriesCircuit这个实例加入到seriesCircuits这个List集合。 -
在parseParallelCircuit方法里面,把并联设备存储起来,同时计算并联电路的总电压。
-
最后是输出,遍历seriesCircuits,判断总电阻是不是-1,若是-1代表断路,直接跳过,若不是,继续遍历,是否是调速器,是的话计算经过调速器的之后的电压,然后用分压法计算各个受控设备的状态。接着又遍历遍历parallelCircuits,计算它内部串联电路的的电阻和电压,更新设备状态。
public void updateCircuits(double voltage) { for (ParallelCircuit circuit : parallelCircuits) { circuit.total_resistance(); } for (int i = seriesCircuits.size() - 1; i >= seriesCircuits.size() - 1; i--) { boolean flag = false; if (seriesCircuits.get(i).getTotalResistance() == -1) break; if (seriesCircuits.get(i).getDevices().get(1) instanceof ContinuousSpeedController || seriesCircuits.get(i).getDevices().get(1) instanceof SteppedSpeedController) { ((ControlDevice) (seriesCircuits.get(i).getDevices().get(1))).update(220); flag = true; } for (int i1 = 2; i1 < seriesCircuits.get(i).getDevices().size(); i1++) { double v = 0;//工作电压 if (flag) { if (seriesCircuits.get(i).total_resistance() != -1) v = seriesCircuits.get(i).getDevices().get(1).getOutputVoltage() * (seriesCircuits.get(i).getDevices().get(i1).resistance / seriesCircuits.get(i).total_resistance()); } else v = 220 * (seriesCircuits.get(i).getDevices().get(i1).resistance / seriesCircuits.get(i).total_resistance()); seriesCircuits.get(i).getDevices().get(i1).update(v); } } for (ParallelCircuit circuit : parallelCircuits) { int i = 0; for (SeriesCircuit seriesCircuit : circuit.getCircuits()) { if (seriesCircuit.getTotalResistance() == -1) { i++; break; } for (CircuitDevice circuitDevice : seriesCircuit.getDevices()) { circuitDevice.update(circuit.voltage * (circuitDevice.resistance / circuit.getCircuits().get(i).getResistance())); } i++; } } }
最后输出和上一次大作业输出一样。
类图
顺序图
2.踩坑心得
-
在开关类toggle方法中,我先判断开关状态设置电阻为0或-1,再取反开关状态导致后面逻辑错误,正确的应该是先取反再判断
-
再判断#M时,通过findParallelCircuitById方法获取并联对象时,传入Id的值出错导致获取不到并联对象。
3.改进建议
- 模块化:考虑将不同的功能模块化,例如将设备的创建逻辑、电路的构建逻辑、电路的更新逻辑等分别放在不同的类或方法中。这有助于提高代码的可读性和可维护性。
- 封装性:进一步增强类的封装性,确保内部状态不会被外部直接修改。例如,
CircuitDevice
类中的resistance
和voltage
等属性可以设置为私有,并提供公共的方法来访问或修改这些属性。
-
避免重复计算:在
SeriesCircuit
和ParallelCircuit
类中,总电阻的计算可以缓存起来,避免每次调用total_resistance()
方法时都重新计算。 -
减少对象创建:在
InputParser
中解析输入时,如果设备已经存在,则不需要重新创建对象。可以通过检查devices
映射来确定是否需要创建新对象。 -
命名规范:遵循一致的命名规范,例如使用驼峰命名法(camelCase)或下划线命名法(snake_case)。目前代码中既有驼峰命名法也有下划线命名法,建议统一。
-
集成测试:编写集成测试来验证整个系统的各个部分是否协同工作。例如,可以测试完整的电路配置是否能够正确计算每个设备的状态。
-
接口和抽象类:考虑定义更多的接口和抽象类,以便更容易地添加新的设备类型。例如,可以定义一个
SpeedController
接口,让SteppedSpeedController
和ContinuousSpeedController
实现该接口。 -
工厂模式:使用工厂模式来创建设备对象,这样可以在不修改现有代码的情况下添加新的设备类型。
总结
通过这三大作业,让我认识到了代码逻辑结构设计的重要性。由于上次代码设计的不好,导致这次代码又得推倒上次代码重写,浪费了很多时间,如果刚开始就设计好了,直接在上次代码上增加内容,这样可以节省不少时间。这三次大作业也让我对继承有了更加深入的了解,同时我也学会了JAVA集合的遍历方法和Lambda表达式语法。还有学到了HasMap和LinkedHashMap等集合的异同点。对于什么场景使用什么类型的集合有了更加深入的认识。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~